tortoisehg-4.5.2/0000755000175000017500000000000013251112740014517 5ustar sborhosborho00000000000000tortoisehg-4.5.2/thg0000755000175000017500000001204613251112270015230 0ustar sborhosborho00000000000000#!/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 present - it's not # on 10.9) if len(argv) > 0 and 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 and os.name == 'nt': 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')): # allow setuptools to patch distutils before we import Distribution from setup import build_ui from distutils.dist import Distribution build_ui(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.extend([ 'win32com.shell', 'numpy', # comtypes.npsupport does try-import 'tortoisehg.util.config', 'tortoisehg.hgqt.icons_rc', 'tortoisehg.hgqt.translations_rc', # don't create troublesome demandmods for bunch of Q* attributes 'tortoisehg.hgqt.qsci', 'tortoisehg.hgqt.qtcore', 'tortoisehg.hgqt.qtgui', 'tortoisehg.hgqt.qtnetwork', # TODO: fix name resolution in demandimporter and remove these 'qsci', 'qtcore', 'qtgui', 'qtnetwork', # pygments seems to have trouble on loading plugins (see #4271, #4298) 'pkgutil', 'pkg_resources', ]) 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 from mercurial import util mystderr = cStringIO.StringIO() origstderr = sys.stderr sys.stderr = mystderr sys.__stdout__ = sys.stdout sys.__stderr__ = sys.stderr util.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: util.stderr = sys.__stderr__ = sys.stderr = origstderr raise tortoisehg-4.5.2/COPYING.txt0000644000175000017500000004325413150123225016376 0ustar sborhosborho00000000000000 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-4.5.2/icons/0000755000175000017500000000000013251112740015632 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/menucreaterepos.ico0000644000175000017500000001373613150123225021537 0ustar sborhosborho00000000000000  Ј6 hо  ˜F( @ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЂФ<ЁФlЃЦ“ЇЩН ЅШоЂХїЂХїЅШоІШНЃЦ“ЁФlЂФ<џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЂХ,ЄЧг ЊЫђ6РліgауџŠмыџzтђџ^чљџJхљџ@лёџ5бъџ+ШтџЛиі ЄХђЄЧгЂХ,џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЄЧе,ЩуўWыќџ‹ђ§џПјўџЬљўџ˜ђ§џcыќџOщќџOщќџOщќџOщќџMуіџBУгџ#ЃЙўЄЧеџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџWыќџŠё§џОјўџЭљўџ™ђ§џdыќџOщќџOщќџOщќџOщќџMфіџBУгџ7ЂАџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџVыќџŠё§џНїўџЭљўџ™ђ§џeыќџOщќџOщќџOщќџOщќџMфіџBУгџ7ЂАџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџUыќџ‰ё§џНїўџЮљўџšђ§џeыќџOщќџOщќџOщќџOщќџMфіџBУгџ7ЃАџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџUыќџ‰ё§џМїўџЮљўџšђ§џfьќџOщќџOщќџOщќџOщќџMфіџBУгџ7ЃАџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџTыќџˆё§џМїўџЯљўџ›ђ§џgьќџOщќџOщќџOщќџOщќџMфїџBУгџ7ЃАџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџSыќџ‡ё§џЛїўџаљўџ›ѓ§џgьќџOщќџOщќџOщќџOщќџMфїџBУгџ7ЃАџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџSыќџ‡ё§џЛїўџањўџœѓ§џhьќџOщќџOщќџOщќџOщќџMфїџBФдџ7ЃБџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџRыќџ†ё§џКїўџбњўџѓ§џhьќџOщќџOщќџOщќџOщќџMфїџBФдџ7ЃБџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџRыќџ†ё§џКїўџвњўџѓ§џiьќџOщќџOщќџOщќџOщќџNхїџBФдџ7ЃБџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџQыќџ…ё§џЙїўџвњўџžѓ§џiьќџOщќџOщќџOщќџOщќџNхїџBФдџ7ЃБџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџQыќџ„ё§џИїўџгњўџžѓ§џjьќџOщќџOщќџOщќџOщќџNхїџCФдџ8ЄБџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџPыќџ„ё§џИїўџгњўџŸѓ§џkьќџOщќџOщќџOщќџOщќџNхїџCФдџ8ЄБџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџPыќџƒё§џЗїўџдњўџ ѓ§џkьќџOщќџOщќџOщќџOщќџMсјџAНеџ5šЖџ‘Ъџ&ђ ёџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџPыќџƒё§џЖїўџдњўџ ѓ§џlьќџOщќџOщќџOщќџMуќџHбїџ6œоџ/†Шџ зџLѕW?є4*ђ ё џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџPыќџ‚№§џЖїўџењўџЁѓ§џlьќџOщќџOщќџOчќџBТњџ;­їџ3“хџ1’кџ™фџ,„љ"`іwHє]+ђџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџAнёџRвшџXЪрџFОиџАаџЅЧџЄЧџЏЯџЌйџ"œхџ*–эџ5Џыџ@ЬчџPеёџDоўБ5Іћœ"`і{?є) ёџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџЕдџЉЫџ6ПлџJЪуџ]еыџpрѓџƒыќџƒыќџpрѓџPИьџ8™ъџ0Ÿьџ4ЮяџУђќџиїўџЬіџѓEпџВ-†љMѕ`(ђџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЄЧџDЧсџˆюўџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџŠяџџsЩќџ^ЎћџVЛќџrшџџйјўџћџџџнљџљlцџХ1–њ–Rѕ^,ђ№џџџџџџџџџџџџџџџџџџџџџџџџџџџџџЅЧвDШсўˆюўџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџvЮ§џ_АћџVДќџYфџџЦєўџйїўўЬіџѓEпџВ-†љMѕ^(ђџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЃЦ$ ЅШЦ ЇЩѕ3ОкєIЫфў]еыџpрѓџƒыќџƒыќџpрѓџSНьџ9žщў*шљ%Ішћ5Я№юeт§ЫDоўБ5Іћœ"`і~?єH ёџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџŸХ0ЁФ_ЂЦ‡ ІЩЕ ЅШлЃЦіЃЦі ЅШл ЂЪЖ{дŸkс™jю–,„љ1”њ•,„љ"`і~HєX+ђ" №џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџё*ђ2?єPLѕbPѕhLѕb?єN*ђ5ёџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ № ё&ђ,*ђ.&ђ- ё №џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ№џџ€џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ?џџџџ€?џ№џџџџџџџџџџџџџџџџ(  OБЧџ?ЁЦџ?ЂЧџ=ЁЦџ:›Фџ7•Тџ?ЁРџTАЬџ™пџ4гѓџa№љџOріџ(ХэџІтџyхџ2˜Њџ=†Кџ™пџ4гѓџa№љџOріџ(ХэџІтџyхџ'fГџ=†КџBЦяџoшљџœјќџ№ћџaріџ<Я№џБђџ1Тџ=†КџAШ№џpшљџžјќџŒ№ћџ^ріџ<Я№џБђџ1Тџ<†Кџ=Ш№џpшљџЁјќџŠ№ћџZріџ<Я№џБђџ1€Тџ<…Иџ;Ш№џpшљџЂјќџˆ№ћџWріџ<Я№џБђџ2}Пџ<„Зџ;Ш№џpшљџЂјќџŠ№ћџXріџ<Я№џБђџ2}Пџ=ƒЖџ;Ш№џpщљџЁјќџ‹яћџYріџ<Я№џБђџ$kаџЫоюџ=Еџ;Ш№џpщљџЁјќџŒяћџZріџ<Я№џБђџj™џџmїџОгяџЫоюџ=„Жџœтџ5еѓџe№љџMпіџ%Хэџ‹МџџAџџIџџ7vїџ@ўџ•Бѓџ;žХџ-ЫћџDељџЁјќџŒяћџŒяћџЇЦџџ>‹џџ1бџџ.аџџE€љџЃНђџ=ЊПџ,ЩћџMиљџЁјќџŒяћџaЙќџaќџlљџХѕ§џ”ы§џj§џ7aњџOБЧџ2•Тџ5™Тџ4•Рџ"vЮџ?ќџ?ќџ?ќџ †§џBўџОгяџ?ќџЃНђџ?ќџmїџˆЅєџАШёџЃНђџАШёџј№№№№№№№№№№№№јџРџѓ(  OБЧџ?ЁЦџ?ЂЧџ=ЁЦџ:›Фџ7•Тџ?ЁРџ0—ЩџEЗй/LЂЦџ™пџ4гѓџa№љџOріџ(ХэџІтџyхџ.‡­џ5НП=†Кџ3Зщџ[сїџˆѕћџyъљџNзѓџ.Сыџžэџ-xНџ6ŒЩП=†КџAШ№џpшљџžјќџŒ№ћџ^ріџ<Я№џБђџ1Тџ8‘ЭП<…Йџ<Ш№џpшљџЁјќџ‰№ћџYріџ<Я№џБђџ1Сџ8ŽЫП<„Зџ;Ш№џpшљџЂјќџ‰№ћџWріџ<Я№џБђџ2}Пџ8‹ЪП=ƒЖџ;Ш№џpщљџЁјќџ‹яћџYріџ<Я№џБђџ$kаџЫоюџФяŸmОю?=‚Еџ-Йыџ\тїџѕћџwщљџHзѓџVШѕџ‹іџ_‘џџ[†їџƒЂєџЙЯяџ;•Рџ$Лђџ>еїџŒѕћџvщљџiсіџТџџ.rџџ9Жџџ1Бќџ2jњџžЙђџ=ЊПџ,ЩћџMиљџЁјќџŒяћџaЙќџaќџlљџХѕ§џ”ы§џj§џ7aњџOБЧЊ2•ТЊ7›ФД8šХд(ад?ќџ6hјџ?ќџ9‰ћџ7bњџЙЯяџ?ќUЗђjl’ѕџ™Дѓџ„­ѓП˜Рђ€p00000€џtortoisehg-4.5.2/icons/ignore.ico0000644000175000017500000001246613150123225017620 0ustar sborhosborho00000000000000 h&  ЈŽ(  T. ч џ ўдT,р5џ ЅZ+џ,џ*џ*џУРNџYџ; ј(ъW)џ6џ)џ'џ Ь%јj'џ_ џ<ћ!ЩЁ…xlЂ’ѕ$шu‹fwе–бpL Б˜?}Єhџџџџџџџџќ?Ф€€ЯŸяПѓџћпџяџџџџџџџ( @ (žџџџџќд"xзИ-џ(џ*џ*џ*џ)џ"џ џє8‡ќџ.џџ%‚ џ-џ*џ*џ*џ*џ*џ*џ џ‘Ўџ=џEџHџџqт1џ6џ0џ+џ*џ*џ*џ*џ џ~L џFџMџRџTџD џтЉџFџ*]9џ7џ0џ*џ*џ*џ*џџfЕ0 џOџWџ]џbџbџ> џ1џOџLџ џ*џџ џџџїъоврџВњLџ`’iџ cџb џGџџяЈ~W/Єџ”№EџNџ.џќЖW›џoDVц' џ§Є$’ј/€Фьи6Љг‡ѕжИ аяи;ќC2ќ­ˆр”†ЪІ}Oл‹nџqа§і Жџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ№џўpџ№`џр`џрџРџРџРјџРџќџУџўOЯџџЯџџяџџЯчџџЯѓџџџљџџќџџўџџџџџџџџџџџџџџџџџџџџџџџџџџtortoisehg-4.5.2/icons/menurevert.ico0000644000175000017500000001373613150123225020532 0ustar sborhosborho00000000000000  Ј6 hо  ˜F( @ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ†JЫ‡K3џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џб˜jџб˜jџб˜jџб˜jџб˜jџЮ”fџЩŽ`џФ‰YџН€Pџ›_1ј‰L"ђˆJ g€€џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џб˜jџб˜jџб˜jџб˜jџб˜jџЭ“dџШ^џЛQџŽQ'ѕ‹M! ‡Kџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џб˜jџб˜jџб˜jџб˜jџа—iџЬ‘cџЙ~Pџ‹N#ѕ‰L"hџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џб˜jџб˜jџб˜jџб˜jџЯ–hџЙ}PџŠM"є‡J Hџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џб˜jџб˜jџб˜jџб˜jџЮ•fџМ€RџŠM#ш€€џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џб˜jџб˜jџб˜jџб˜jџЭ“eџЩŽ_џ‘U*ѓ†J"LџџџџџџџџџџџџџџџџџџџџџŽGџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џб˜jџб˜jџб˜jџб˜jџЬ’dџШ^џЎqDџŠN$ЧџџџџџџџџџџџџџџџџџџџџџˆK!ДˆK џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џб˜jџа—iџб˜jџа—iџЫ‘bџЧ‹\џС…UџŠN#і†M (џџџџџџџџџџџџџџџџџџ‡I1‡K §ˆK Žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џб˜jџЃh<џЯ–hџЯ•gџЪaџЦŠ[џС„UџЂd8ќ‰M#ЉџџџџџџџџџџџџџџџџџџџџџˆK ‡J џˆJZџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џХ‹^џ‡J џЊoCљЮ”fџЩŽ`џХ‰ZџРƒTџИ{KџO$ѕˆK iџџџџџџџџџџџџџџџџџџ†J"L‰K"ћ‰K!ыŽU џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џЇk@јˆK"ŸŠM#ђР†XџШ^џФˆXџП‚RџК|LџЏqAџ‰L"ѕ†I ?џџџџџџџџџџџџџџџ„J!ˆK!ї‘S'єˆJ xџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џŠM#ї‡H ŠJ#`“V+ѓЦ‹\џУ†WџОQџЙ{KџЕuEџІf7џˆL"ѓ‡Kb€€џџџџџџџџџ™33ˆJ ўž].џŠM!Шџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џŒO%Цџџџџџџ‹N%УЇl?ќТ…VџН€PџИzIџДtCџЏo=џЄc2џQ%ѓ‰K!р†J"Lџџџџџџ…HC‹N#ѓЅd3џˆK ѕџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡J џ‡JSџџџџџџ„L‰M#ъЊm?§М~NџЗyHџГsBџЎm<џЊh6џЉg5џŸ^/џ‹M"є‰K!у‰L"ХˆK!їŸ^/џЅd3џˆK!ђџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‹L#Q™33џџџџџџџџџ‡K"‰M#ьЈi<ўЖwGџВrAџ­l;џЉg5џЉg5џЉg5џЉg5џЂa1џž^.џІd3џЉg5џž^.џŠL"ЬџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџŠL"%ŠL"эЃc5ўБq?џЌk9џЉg5џЉg5џЉg5џЉg5џЉg5џЉg5џЉg5џЉg5џŒO$ѓˆK Xџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ†J"&‰L"дR&ђЅe4џЉg5џЉg5џЉg5џЉg5џЉg5џЉg5џЈf4џ‘S&і‰L!Рџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ‡K!fˆK!ё“U(єœ\-џЃb1џЈf4џЄc2џ›[,§‹M#ѓ‰M!Њ™33џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ†Q‡KjŠL"Ќ‰K!ч‡J!ћ‰L!хˆK!АˆK:џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџќџќ?џќџќџџќџџќџџќџџќќџќўќўќџ?ќ?ќР?ќР§рџ№џј?џќ?џџџџСџџџџџџџџџџџџџџџџџџџџџџџџџ(  lllџК<џxxxџ€€€џК<џК<џ’’’џ€€€џК~џК<џК~џК~џК~џК~џК~џК~џК~џК~џxxxџй“џК~џК<џК~џџ…Hџ’’’џК<џК<џй“џК~џй“џК~џК~џК<џ№ЊџxxxџК~џй“џК~џК<џК<џџЖ$џК~џй“џК~џџ…HџК~џК~џК~џџ…Hџџ…HџК~џй“џК~џ›iџџџџџџџџџџџџрGрgсчрчьюџџџџџџ(  lllџК<џxxxџ€€€џК<џК<џ’’’џsIŸ€€€џК~џК<џК~џК~џК~џК~џК~џК~џГ{ŸК~џК~џxxxџй“џК~џК<џК~џџ…Hџ’’’џКVПК<џК<џй“џК~џй“џК~џО}ŸКcПК~џК<џ№ЊџxxxџК~џй“џК~џЧ}Пд~ ПКqяК<џК<џџЖ$џф‚3К~џй“џК~џџ…HџК~џК~џК~џџ…Hџџ…HџК~џй“џК~џ›iџџ№џ№ќpќ0ќ0€ƒ€И0џ№tortoisehg-4.5.2/icons/16x16/0000755000175000017500000000000013251112740016417 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/16x16/status/0000755000175000017500000000000013251112740017742 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/16x16/status/hg-removed.png0000644000175000017500000000074113150123225022505 0ustar sborhosborho00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“ pHYs  šœtIMEп-%ыі6 nIDAT8ЫЭ’AKa†ŸѕзB мDЂCбa=дЁŽС.DїюўŠ~„П'ˆ0кМFPaQЧHЌTPX:ьђiгСvС\Лжœ^˜yоoО™up.*Љƒ“”k”Ы‰ЙИрЩqФ?:’ЧННЉТ:8їЖ-§н]imnNхŒH\T*ВcлШxŒhЭkЗЫЩе• PЕmЯ2MDkDkюњ}z= \?<ИЅЅ%Џ”Я#ZГВА@ukЫCkЌl6†п}Ÿл^Я8‰<ЏНМ4Kљ|-—N#Z“S S)d4ŠсГvл=†ЫƒиЄгiЎ,.жrJM^§H%NєЛнŽtRЈŸ›ЈкЖgeГгАж˜"•Њ­AГЯ3I№Ляѓ˜"“ЯOŠЉTmCыи$^ccuUЖ-kцЯћ…‚W4 h‡ahLЭрІнvпƒ™УхљpшvУ€юhD+ нЙзxЖМ<ї”O3™љЇќgёepъjўЉЏJIENDЎB`‚tortoisehg-4.5.2/icons/16x16/mimetypes/0000755000175000017500000000000013251112740020433 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/16x16/mimetypes/text-x-generic.png0000644000175000017500000000051513150123225024003 0ustar sborhosborho00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“ pHYs  šœtIMEе8К`&BкIDAT8Ы­’=n„0…ПHЛ[!!ŸŸГl™"ЧAц!Qкsњ­LŠˆ$^XЂМj,{о|3Еmћ1 У ЧєZзѕ;жкщЈЌЕгь”ЬС8Žxяt/5Ч’ˆу˜4MW(wя=ЮЙUТR’ЈЊъW/ЩђPХ&AHЩђс’рЇž"ШѓЈ‹ЛIDAT8ЫЅ“ЯkA†ŸэnЗI šbk5%ЅPт% ЂлЋз–™ЭєŽqНјƒЊзЄ^VдЫЊ;pљЄ‹iX\˜Мэ3§ЉЛOоъ(&ZНW< TњV<Щ/]њКе(§ЧЛIENDЎB`‚tortoisehg-4.5.2/icons/16x16/actions/hg-add.png0000644000175000017500000000061113150123225021705 0ustar sborhosborho00000000000000‰PNG  IHDRѓџabKGDџџџ НЇ“ pHYs  šœtIMEп( чяsIDAT8ЫЭR=KA}{Є“У‹!—&G"иљ ,$јCв^№џС6џФТоJмYD8ЎиcwgЦтєˆ1KˆN30oцЭ<рЏMљ“tЮу$†%Ц2/№pŸћъ:>€бАыЋKЉ СXі | G‚кД!8bќ€ЁeIп`’Юy4ьУ‘Р#єzа†НX­оa‰сHTU–ђД˜пn0Nт–ѓзфOАЁsЖ1ЪђŸ,1і5ыXmn‹RЛС2/`,+G K‚гГa7‚6„тm­6ы  XuUжСЭэ‚“‹shC*{~СунTѕл|Aщ•бAЫ™ХрUbU–BYоj‹ѓџГШ‚Ю…s ІIENDЎB`‚tortoisehg-4.5.2/icons/16x16/apps/0000755000175000017500000000000013251112740017362 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/16x16/apps/reviewboard.png0000644000175000017500000000037413150123225022403 0ustar sborhosborho00000000000000‰PNG  IHDRѓџaУIDAT8Ыcшd’т@ќˆџ‰?CѕШ0LŠa›ГЃЧэџГЃџП>=“( R в2„H| l\6хПETƒиФr ШАHЃ^Ь,0жЌќ?Їž'†в‹еЋиІџ^њcХX xЙ3яџњ™e`›AšїюŽџџъІ/†RТ(€0H—ЭШ. h6лGš (2€ŒnРч'[2р‚ФbpRžЭ2{{‹бR Љщe&ŠВ3rЃ!є(…IENDЎB`‚tortoisehg-4.5.2/icons/16x16/apps/gnupg.png0000644000175000017500000000127413150123225021212 0ustar sborhosborho00000000000000‰PNG  IHDRѓџasBIT|dˆ pHYsььu85tEXtSoftwarewww.inkscape.org›ю<9IDAT8“OH“qЧ?Пзwk:чаe*QъB2-,0ђtщT‡Є„‚ (VQtTŠ(:uШаƒбБKPt 2еA Т$ТIPЫй–т6яЖї}O5нД=№;=пячљУѓS"BaЈОёЃ[Нfч*пaGДw$žœЭИЯ$~КA[№їOоКоRж5gЙj№Л…Аa:эrG"ЛКў PНпŽwя=є%E4эЦ@=ё€: @ш”KсЧЋc=­вчЙљёW–hкЦЫn‰д_–Hј.ЭРќrЎ­їќЈžёжE[[/&­wˆtЩљpr5'Wъc 7€!РRНSЭЋ9s %Ы‘ƒ+иž ›EйР!Дl:Тџ„  њ‡=ЈP-ЂG—{6*еƒ‰Ж<Ѕa=№Fwk№D >WAZ‰ЊoЂ­gўUЉЎЬ$В/ШЙІХІb`NUлFЬ,nї›ь yh,ївXюYy^ЊKŠKиt ФБьЄœ­[[ЂЏHqЛ­‚Ћ-A ЕБƒЏѓ6їF|ˆg)ѕЌ­ЮhпЙХ{ЗЕœіпІЦ—SK$Вš3{йQЬщWq№k€SЕСЪЭЬРЪAЦ6GSмџ”BЏЛ~РбшBуLкхmдт§Я,бДУE—І %І"™“|@Fr%y€ Ѕt4”ВфSK\|3K2—_Ы0•Ж-XŽШыщLv8žqm‘…œ–ё”эЭdьЌKўЗ]pр7нhіВv9ХСIENDЎB`‚tortoisehg-4.5.2/icons/16x16/apps/kiln.png0000644000175000017500000000136013150123225021023 0ustar sborhosborho00000000000000‰PNG  IHDRѓџatEXtSoftwareAdobe ImageReadyqЩe<’IDATxкbјџџ?cВІžЫєC‹_Ш;oE—ƒыУmkѕь]ў/МјэаЬ‹џЅљ˜eБР„Эf5†дwЅ/Љї ќ/N2ш}8Р`gЂлЩТР Р€Ы)ŒLЬZ цdдАgАt•чл{ЋVїџџЩџл\%оKБOQ`qТъ%Нt5ЧАЫ@9$ѓy Eџ?Mчј§ПMљЯџNеO*ўˆГ%`xсЧ—wlтъ&:Ўesї;цOўЎэ•Д(ЬќмЖњєnЫ­юНwэXјY8XйaњX` FFІ2j bj† ппПт3vq“жЕ~'$ЋЦјвРв}W§ўиЅлŸп_|хзŒOЮФ0€™•§еы;ч€aСФР+*Э№ўб ^1yЦЯ0(ъйpМїK=ГaЖz2ТЂ‰‰™ELЭh3+›ЗЂЅƒ”Ž5УїЏ~џќЮР/ЉШ№чз†uХЎ:@ЅWaж‡ˆо=КQ*oцСРТЮС№яЯ/v>!!9u>qy†зЗЯƒ”ёЃЛ%ќўўхњЋ+‡л%ў№Ы2ќћђ†свљs —6Яd8:ЛВЈфЮt€ ‚}мŽХ&g>e`фЮr“gIF“””Ѕ‹MD§Aь(і5œЌLђИ Рž”Ѕxн>ќcљ b+‹qѓјЈsХђА1Ъ™ЩА‡* Вz…y0bA–ŸеЊеEp­<Ћ„НУСГŸћМ€…‰‘гA‘#\€ƒщЗ‰4;ыЬ3ŸzŽ~Lџ№§я,ЄјXL9уфX52MљŒN>§Сњюл?цдMЏ•˜^J№А•qНћўїЭЏПџŸў§їьB€sздeѓ8IENDЎB`‚tortoisehg-4.5.2/icons/menurepobrowse.ico0000644000175000017500000001373613150123225021412 0ustar sborhosborho00000000000000  Ј6 hо  ˜F( @ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ64.R53.с54/Ч64.(џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ64.[53.ѕ+3=џ1?Gў661ю64.(џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ64.Z53.ѕ*4?џ-FYџ3YpџD^lў883ю64.(џџџџџџџџџ€џЂФ4ЊЩcЎЮ˜ ЌЭЛ ЋЬ  ЈЫlЁХ9ŸПџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ64.Y53.ѕ*4@џ.GZџ3ZqџMuџi‘Љџd{‡§;<8ШџџџџџџЂФЏЯеDЬуё•тяџщіџNшќџHтїџ<ияџ/ЧрѕЊЩмЉЪ>џџџџџџџџџџџџџџџџџџџџџџџџџџџ64.Y53.ѕ*5@џ.G[џ3ZrџNvŽџj’Њџ†ЏЧџs‘Ёџ;;7рџџџџџџ ЌЫ“CпѓџwяќџЪљўџЃє§џPщќџOщќџOщќџLпёџ:­Мџ ЌЭОџџџџџџџџџџџџџџџџџџџџџџџџ64.X53.ѕ+6Aџ.H\џ3[sџOwџk“Ћџ‡АШџz›Ўџ;<8є750Jџџџџџџ ЋЭ˜FтѕџwяќџШљўџЄє§џQщќџOщќџOщќџLпёџ:ЌКџ ЌЭСџџџџџџџџџџџџџџџџџџџџџ64.W53.ѕ+6Bџ.I\џ4\tџPxџl”ЌџˆБЩџz›Ўџ;<8є75/Oџџџџџџџџџ ЋЭ˜FтѕџvяќџШљўџЅє§џRщќџOщќџOщќџLпђџ:ЌКџ ЌЭСџџџџџџџџџџџџџџџџџџ64.V53.ѕ+6Bџ.I]џ4]uџQy‘џm•­џˆБЩџzœЏџ;<8є75/Pџџџџџџџџџџџџ ЋЭ˜FтѕџuяќџЧљўџІє§џSщќџOщќџOщќџLпђџ:ЌКџ ЌЭСџџџџџџџџџџџџџџџ64.V53.ѕ+7Cџ/J^џ5]uџRz’џn–ЎџˆБЩџzœЏџ;<8є75/Qџџџџџџџџџџџџџџџ ЋЭ˜FтѕџtяќџЦјўџЇє§џTщќџOщќџOщќџLрђџ:ЌКџ ЌЭСџџџџџџџџџџџџ64.M53.ѕ+7Dџ/K_џ6^vџS{“џo—ЏџˆБЩџzœЏџ<=9є75/Qџџџџџџџџџџџџџџџџџџ ЋЭ˜FтѕџsяќџХјўџЈє§џUщќџOщќџOщќџKишџ9™џ"n{м75.t73/A333џџџ53.р-8Bџ/K`џ7_wџT|”џp˜АџˆБЩџzœЏџ<=9є75/Rџџџџџџџџџџџџџџџџџџџџџ ЋЭ˜FтѕџrяќџФјўџЉє§џVщќџMжчџ@jiџ782џ@LCџLdPїU[>пAA2у75.љ:9/Х540а2BKў8`xџU}•џp™БџˆБЩџ{Аџ<=9є75/Sџџџџџџџџџџџџџџџџџџџџџџџџ ЋЭ˜FтѕџqюќџУјўџЊє§џKЈАџ;E@џ[џyвКџ~дДџy͘џr­ƒџwАwжВЎC‘li7Н96/ђ661іKerўqšВџˆБЩџ{Бџ<=9є75/Tџџџџџџџџџџџџџџџџџџџџџџџџџџџ ЋЭ˜FтѕџpюќџТјўџ‚БЕџ?OJџhУЗџtоаџvмЬџxиУџsПЄџk­џoБ†бИЖI„ЖВD™–?žA>0ч984ѕg‹ўt“Ѓџ<=9є75/Tџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ЋЭ˜FтѕџoюќџВхъџ@E?џdРНџlсоџmсмџoрйџqмбџkСЏџdЎ—џfВ’ЫЛЙUuИЖM‚ЖВD™–?ž97/ђ<=9Я;;7р75/Mџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ЋЬš+Фпџ0Окџ1п55->џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ74.ѓ-• [4­ОzŸв՘тэрСџџџџєєуГщщЧhлл­нн™ЯЯ ЬЬ€2ЪЦvCХТjTЖД\i75.њ;;' џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ76/ЂA?6ŽееЊууЗ@ььаƒєєуГёёоЈъъЪnссА*ббЂ бб’вЬ‚-ЪЪw>ХХnOa_?І;8/Зџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ99+75/фYYFBлл’ууЗ@щщЧhъъЪnцуОRммЇее• ааŽааˆ+ЬШ{<Ui96/ъ;11џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ55.M86/рYYFBееЊлл­ссА*ммЇееЊвв–ее‘вЬ‚-„V_;90п66.^џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ55.M75/фA?6Ž€€` ббЂ ее• вв–жжЁœr1OM<•85/ъ64.Yџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ99+76/Ђ74.ѓ86/р<:2Г>=2Б86/о64.є76/Џ333џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ771*55-`64.c72,.UUUџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџчџџџУџџџќџрўРќРјР№РрРРР€?РРџРџРџР‡џРЧџрчџјїџјїџјїџћїџљчџ§џяџўџпџџ??џџ€џџџџџџџџџџџџџџџџџџџџџ(  T|џEEEџRRRџT|џi›џ?]џ<Јџ; Мџ;ŸМџ+ŒЖџRRRџT|џ~КџlЮџџ?]џ0˜аџMнѕџMнѕџTпѕџ Бшџ|ЪџRRRџT|џ~КџlЮџџT|џ?Ідџ}ьљџ}ьљџ„эњџCађџ)•жџEEEџT|џ~КџкџџT|џ;Ідџыљџыљџ___џ999џџ999џ___џ*>џ“йџкџџT|џ;Ѕгџьљџxxxџtшјџ7Цяџ'гџџкџ’’’џџT|џ?]џ6ŸаџRRRџpшјџtшјџ7Цяџ'гџџџџџџцДџ’’’џ___џ0Љйџџ[пјџtшјџ7Цяџ#ŠЮџЄ  џџџџџџкџ999џџLИкџLИкџLИкџыыыџџцДџјјјџџкџџ999џјјјџоооџРРРџ___џРРРџxxxџџкџ999џlllџџцДџјјјџџџџџјјјџџцДџџцДџџкџlllџxxxџџцДџјјјџыыыџџцДџџцДџxxxџlllџ999џџ999џlllџџќџ№УрСƒ€€€€РРРр?№џџџџ(  D‰Ђ=xZcˆо/GSџZЉОR ЕvKДЄE—ВЄF“Џ?Kw‡Ѓ#WqџvІџMoџKІЦ CДЭџDКдџ,ЁЭџ0’ХъK‘Џ/S}Š–%Vnџ}Аџ7œЭџ/jƒЭBЎйџhхїџlцїџ?Ъяџ/ФџJ}“oUz… )VkџzЌџCЁЯџ7z˜Ш[‚ŒIГлџ~ыљџqЏЗџEksџ6Fџ\fdџ7MWџqЅџZБмџ9}›б^‡•HГкџ|ФЬџtЖПџBИзџH‡ЋџПЌ†џGKLџPtџ'c~цd’ 8ŽЕўU†џqчїџCЬ№џO иџїюнџЦЛЃџgfdџ^ˆ?8‰ЇФ"T_џaзяџEЦъџoЈЬџзбШџўшЛџSL=џ\}…?i”2JcjўЬрџ€НбџЙЖАџеЭМџцЯЃџF?0џp–?wЁЊ3˜’‡џѓэсџхххџПМЗџзЩЌџчЬ”џwpaџ…АЖš•‘зЩЎџњї№џђэуџџцДџХД”џ”’mŠЈЉ‚ЇЇ]\Zў++)ўfc[ў{„ЂЃџРЯ0p№№€№€№№У№tortoisehg-4.5.2/icons/menusynch.ico0000644000175000017500000001373613150123225020347 0ustar sborhosborho00000000000000  Ј6 hо  ˜F( @ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNšNjQРœOѓ›OєœO№PЫ›O‡šN.џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšN_œPђЌ]єМiџЧqџХpџТlџЗdџ Ћ[ћœPѕœPеšNašN џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšN QП ЂUє"Щuџ(д}џ'в{џ$аyџ"ЭwџЪtџФnџА_§ŸRђšN§šNџ›OšџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNQСБ`і,й‚џ-кƒџ,йџ*з€џ(д}џ%бzџХpџ ŸRєœPІšN?šNšNšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ›OŒšNџџџџџџšNPВ­^ѕ0м…џ2п‡џ2п‡џ0н†џ.лƒџ+з€џСnџPє›OUџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџšNkџџџџџџœP…ЈZє0м…џ4сŠџ6уŒџ6уŒџ4сŠџ1о†џИgњPшšN-џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџPмџџџšN+Pі+е~џ3рˆџ7фџ:шџ:шџ7фџНkњžQЯšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџŸQєšN7QбОk§/м„џ3р‰џ7фџ;ш‘џ;ш‘џ+б}џQяšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџГa§›Oх ЅWє*жџ.кƒџ1о‡џ5тŠџ7фŒџ3н†џ ŸRѕšNAџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџФnџœO§Чrџ(д}џ+иџ.л„џ1о†џ2оˆџЊ\єP…џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџЦoџПkџ"Юwџ%бzџ(д}џ+з€џ-й‚џСoўQЯšNџџџџџџџџџџџџџџџџџџџџџšN šNšNšNšNšNšNšNšNšNšNšNšNšNšNџУmџЧqџЫtџ"Юwџ%бzџ'г|џ(г|џžQѕšN6џџџџџџџџџџџџџџџџџџџџџџџџšN?šNђšNў›Oј›Oј›Oј›Oј›Oј›Oј›Oј›Oј›Oј›OјšNџšNџСjџФnџЧqџЪsџ!Ьvџ#ЮxџЕdћPДџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšN.›Oэ ­\ќНgџНgџНgџНgџНgџНgџНgџНgџНgџšNџšNџНgџРjџУmџЦpџШrџЩrџPљšNNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNœOс­\њТlџТlџСkџРjџПiџНgџНgџНgџšNџšNџНgџНgџПiџТkџФmџХoџЗdўœOюšN"џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNœPхДaќЧpџЦpџХoџФmџТkџПiџНgџšNџšNџНgџНgџНgџНgџПiџРjџСkџБ_ќœOхšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšN"œPюЛhў ЫtџЪsџШrџЦpџУmџРjџšNџšNџНgџНgџНgџНgџНgџНgџНgџНgџ ЋZњœOсšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNNPљ#Яxџ#Юxџ!ЬvџЪsџЧqџФnџšNџšNџНgџНgџНgџНgџНgџНgџНgџНgџНgџ ­\ќ›OэšN.џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџQДИfћ)е~џ'г|џ%бzџ"ЮwџЫtџЧqџšNџšNџ›Oј›Oј›Oј›Oј›Oј›Oј›Oј›Oј›Oј›OјšNўšNђšN?џџџџџџџџџџџџџџџџџџџџџџџџšN6 žRѕ,зџ-й‚џ+з€џ(д}џ%бzџ"ЮwџЪsџšNџšNšNšNšNšNšNšNšNšNšNšNšNšNšN џџџџџџџџџџџџџџџџџџџџџšNžQЯ!Фrў2пˆџ1о†џ.л„џ+иџ(д}џФoџ!ЬuџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџP…Ћ\є7уŒџ7фŒџ5тŠџ1о‡џ.кƒџ$ЬwџœO§ЩsџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNA ŸRѕ3н‡џ;ш‘џ;ш‘џ7фџ3р‰џ/м„џ ІWє›OхЕdќšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNPя(Юyџ7фџ:шџ:шџ7фџ3рˆџСn§QбšN7 ŸRєšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNQЯКhњ1о†џ4сŠџ6уŒџ6уŒџ4сŠџ-з€џQіšN+џџџPмšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšN-PшЕdњ+з€џ.лƒџ0н†џ2п‡џ2п‡џ/л…џЉZєP…џџџџџџšNkšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ›OUœPєНjџ%бzџ(д}џ*з€џ,йџ-кƒџ-й‚џ­]ѕPВšNџџџџџџšN›OŒџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNšNšNšN@œOІŸQєРjџЪtџ"Эwџ$аyџ'в{џ(д}џ(д}џЏ_іPСšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ›OššNџšN§ŸRђ ­\§ОhџУmџЧpџЩsџ!Ьuџ"ЮwџХpџ ЂTєPПšN џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšN šNaœPдœPѕ ЊYћГ`џМgџРkџСlџИeџЊ[єœOђšN_џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšN.›O‡œPЫ›O№›Oє›OѓPРšNjšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџрџџ€џўџќџxџџpџџ0џџ џџџџџџ?џџр№џјќ?ўџўўџџќџџјџџјџџ№џџр џџРџџ€џў?џрџјџџўџ(  |(џ|(џ|(џ|(џ|(џ|(џ›2џК<џК<џ›2џК<џ|(џ|(џ|(џHџ…џHџ…џHџ…џ|(џ|(џ|(џ|(џ$џmџHџ…џ$џmџ|(џ|(џ›2џК<џ$џmџК<џ|(џ|(џК<џ$џmџК<џ|(џ|(џ|(џ|(џ|(џ|(џ|(џ|(џ|(џК<џК<џК<џ|(џ|(џ›2џ›2џ›2џ›2џ|(џ|(џ›2џК<џК<џ|(џ|(џК<џК<џ›2џ|(џ|(џ›2џ›2џ›2џ›2џ|(џ|(џК<џК<џК<џ|(џ|(џ|(џ|(џ|(џ|(џ|(џ|(џ|(џК<џ$џmџК<џ|(џ|(џК<џ$џmџК<џ›2џ|(џ|(џ$џmџHџ…џ$џmџ|(џ|(џ|(џ|(џHџ…џHџ…џHџ…џ|(џ|(џ|(џК<џ›2џК<џК<џ›2џ|(џ|(џ|(џ|(џ|(џ|(џџџј?№`Aџџ€РрррџРџ‚ўјќ(  0К<џК<џ›2џ0|(џ00Hџ…џHџ…џ|(џ|(џ’4_|(џ|(џ$џmџHџ…џ$џmџ|(џ0|(џК<џ$џmџК<џ|(џ™5|(џ$џmџК<џ|(џ|(џК<џК<џ|(џƒ+џ|(џ|(џ|(џ|(џ|(џ|(џ|(џ|(џ|(џ|(џ|(џ|(џƒ+џ|(џК<џК<џ|(џ|(џК<џ$џmџ|(џ™5|(џК<џ$џmџК<џ|(џ0|(џ$џmџHџ…џ$џmџ|(џ|(џ’4_|(џ|(џHџ…џHџ…џ00|(џ0›2џК<џК<џ0ј№p№№№№џўќ№рё№tortoisehg-4.5.2/icons/expander-open.png0000644000175000017500000000027413150123225021106 0ustar sborhosborho00000000000000‰PNG  IHDR ЉЌw&ƒIDATxкcd 0‚ˆ‰'fЉщxдeцччЯ€)ўŸSхдЉS€ŠБ*жu+cИМЋ ЗтЬЌ,ИЄО{9УХpўєiг0ƒЁ& Х11бp6Ёm GVWСљK–,EU—t‹яgиЕАЮ_ЗnnХш]1ёсL,zJ ,}`OIENDЎB`‚tortoisehg-4.5.2/icons/svg/0000755000175000017500000000000013251112740016431 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/svg/sync.svg0000644000175000017500000001207213150123225020126 0ustar sborhosborho00000000000000 image/svg+xml Sync 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg tortoisehg-4.5.2/icons/svg/refresh_overlays.svg0000644000175000017500000001444013150123225022535 0ustar sborhosborho00000000000000 image/svg+xml Refresh Overlay Icons 2009-05-31 Peer Sommerlund TortoiseHg menu icon for refreshing overlay icons tortoisehg-4.5.2/icons/svg/ignore.svg0000644000175000017500000001613313150123225020437 0ustar sborhosborho00000000000000 image/svg+xml Ignore 2009-02-28 Peer Sommerlund "Ignore" icon for TortoiseHg tortoisehg-4.5.2/icons/svg/recovery.svg0000644000175000017500000003076413150123225021020 0ustar sborhosborho00000000000000 image/svg+xml Recovery 2008-05-26 Peer Sommerlund Icon for TortoiseHg dialog "Recovery" tortoisehg-4.5.2/icons/svg/detect_rename.svg0000644000175000017500000004723513150123225021762 0ustar sborhosborho00000000000000 image/svg+xml Status 2008-04-16 Peer Sommerlund Icon for TortoiseHg dialog "File Status" image/svg+xml tortoisehg-4.5.2/icons/svg/checkout.svg0000644000175000017500000002355313150123225020765 0ustar sborhosborho00000000000000 image/svg+xml Checkout 2008-05-24 Peer Sommerlund Icon for TortoiseHg dialog "Update" tortoisehg-4.5.2/icons/svg/commit.svg0000644000175000017500000001121013150123225020433 0ustar sborhosborho00000000000000 image/svg+xml Commit 2008-04-09 Peer Sommerlund Commit icon for TortoiseHg tortoisehg-4.5.2/icons/svg/thg_logo.svg0000644000175000017500000012624013150123225020757 0ustar sborhosborho00000000000000 image/svg+xml TortoiseHg 2007-dec-11 Peer Sommerlund Closely resembles TortoiseSVN logo Hg tortoisehg-4.5.2/icons/svg/proxy.svg0000644000175000017500000003464113150123225020341 0ustar sborhosborho00000000000000 image/svg+xml Proxy 2008-04-19 Peer Sommerlund Icon for TortoiseHg dialog "Web Server" image/svg+xml tortoisehg-4.5.2/icons/svg/shelve.svg0000644000175000017500000002505013150123225020440 0ustar sborhosborho00000000000000 image/svg+xml Status 2008-04-16 Peer Sommerlund Icon for TortoiseHg dialog "File Status" image/svg+xml tortoisehg-4.5.2/icons/svg/log.svg0000644000175000017500000002246413150123225017741 0ustar sborhosborho00000000000000 image/svg+xml Log 2008-05-19 Peer Sommerlund Log icon for TortoiseHg tortoisehg-4.5.2/icons/svg/add.svg0000644000175000017500000001237413150123225017707 0ustar sborhosborho00000000000000 image/svg+xml Add 2008-04-09 Peer Sommerlund Add icon for TortoiseHg tortoisehg-4.5.2/icons/svg/repobrowse.svg0000644000175000017500000003024013150123225021336 0ustar sborhosborho00000000000000 image/svg+xml Repo Browse 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/svg/clone.svg0000644000175000017500000002030213150123225020245 0ustar sborhosborho00000000000000 image/svg+xml Clone 2008-03-02 Peer Sommerlund Icon for TortoiseHg dialog "Clone" tortoisehg-4.5.2/icons/svg/init.svg0000644000175000017500000002272513150123225020123 0ustar sborhosborho00000000000000 image/svg+xml Initialize 2008-12-27 Peer Sommerlund Icon for TortoiseHg dialog "Init" tortoisehg-4.5.2/icons/svg/remove.svg0000644000175000017500000001524713150123225020456 0ustar sborhosborho00000000000000 image/svg+xml Remove 2008-05-07 Peer Sommerlund Remove icon for TortoiseHg tortoisehg-4.5.2/icons/thg_logo_92x50.png0000644000175000017500000002003313150123225021005 0ustar sborhosborho00000000000000‰PNG  IHDR\2бМrsRGBЎЮщbKGDџџџ НЇ“ pHYsHH1тXItIMEи EM‚›IDATxкэœy”]U™іћœ;п[cRCЊRЄ** 0“ A†@ЋД6ъBдVСЏADЄ•FDхГЛD!ˆ@#2 C˜Bц‰ЬU•ЄІ[w8гоgяўумЊhЗŸ8€ИОхЛж^u‡““SЯyЯѓ>яА ўn‰e€-ЫъЗ,ЫЮ~МмŒџ;LяŽ%€ч&Lя4Wмz‹љзŸ.3G№4˜…KN6ŸћЗыЬ'Ÿd€~`ц№?ВџŽлŸfVЪJЁљpЧи‰f|§ŽгвбN]C‡w<ћїбзнУ?|ё Ь_МщћщMoЎ:ИаЋ€лРX`20˜LZ€4рС_§ЂтvГQцЩжЖж ŠCХъKўѕ›ДŽџ-Ц0eЮю§ої™2w‰d’Y ђмЯitЫхЗ€ +зg?VE!„ЩfГІЅЅХДЗЗ›ƒ:Шдзз›X,flюў h}Я=;nUY–ѕц1‹1пЛх{ІОБбмЛъ Гlѕ*Гlѕ*ѓ Ю7ч}і3fйъUцЈгN5u ЦŽХЬЗюПЯ,љшG p@ь}цРг‹ЧŽ›<юИу8ќ№У™1c'MЂЖІлЖ1Рр8{іtйkжЌžДrхЪIПњеЏЮ_П~Н2Ц<м ќWх†М{v˜хцКЮY‡мxѓмўŸЗ3iжL„eаЕm;П\vБXŒХKЯурYГXё‹GxsХ ЦO›FхI}_Џ.>3fܘж .И€ЅK—2sцLŒ­CТP†!ŽуЂЕFkƒС K0ІЅ…ЖЖ6NћрљжЗОЭ–-[bЫ–н{ТmЗнvBooя6рыР=€~7.X,SlлўФ5з_ƒHіюлK]CУШї=;w€(ЅшэъЂЎБqфЛТР SЇTџЕŸЎъььlјђ—ПЬYg}ˆXЬF)EЙь Ѕ$$~ ЅDJ… aЈ1к€!БXŒxјQkkыьkЏН–sЯ=­ AP*•№|Яѓё<Яѓёƒп"Р•B…!Zk†ЫЖˆЧb$ вщ4йLš\.K.›хТ џ‘ѓЮ;ЋЎКъАлoџбЋZыЏпсЄ?б’$cRШsЯ8я ‚0 ”ЩдeшZЛwф˜ŽЮiФуq2ЙMЕБnхЪ‘я&ޘЮцUoЌћk.€Ы,ЫњцХ_œјЦ7Ў!•JтКŽутИŽутКЎчсћа”‘‡+EЈBTбŒжfФ{„иЖM<'J‘ЫfЉЉЉЂЖІšкšjО}нuœrъ)Щ‹ўщЂћћћцЋЈ›?Щфœ—Щfšц9Oy”e™1сё{~‰C,лІКЎŽKoќ.щ\ŽX<ЮЦ7пфиГNgЪœ9Ь<тюКюz€ЧоkРГРнMMMgоuз]{ьБxžЯPЁHБXЂT*Qv\\з@†V* QАuЈGМ\k3тэƒ%,lл&™LЩЄЉЉЎbT}ЃGе1gЮЁ<ѓы_sўљK—Ў_ЗЎ 8(ќбЪфУт>qќЌЙГˆ%cмŽthэl%дЏ?ћ‡žp<ГŽ\РP?o<ї<—пv;gёѓ[oЇЗЛ{K%˜Пg‰O#А|юмЙ'<ѕдStvvтК.…B‘Сќљќ…b‘rЙŒыVhФ№ƒˆУЉ*РX!R…HUyЏ$A №ЅФѓ\зЇ\v(–JЅ2Žу!ЅЂІІ†ѓЯ?Ÿ•ЏМвбееЕИЬ/qь%ЧŠOьМіœŸ3nЪ!Sђ‡ШЛyЪЊL(B–пёsžВ„d*€жš[ЎКЉ]І>‡_нћSžјЩНЎбцt ;‡љwлš€_xт‰3юПџ~’Щ$ŽуR(Щ(Š8ŽƒWёъ]{щэЫгдPC6•‰h$ŒРе:DJ‰eYxОЊЈ}РгЭš`[‰D‚l6УшQuДЖ41Ў­…t*Сй:‹еЋпzЄЂљ5‹(2˜џХёЭЯЧˆѕ=§]3І†}Ѕ}ь+эcШТё–]Е З7фьO_LІЊŠхЫюcЫњ7™Еdƒ]ƒ(ЅШŽЮ†хђ—'5~oХ-+ЬЛэсUРђ“N:щ|D"уИ  ‘WŠ”‡СЎаЧін=ьщюЅЎ:WQ,!ŒшЄgп>^zх T(IЇГH}§Œh'тјˆr†Ÿ€@J<ЯЧѕ<‚@‚ккЮ=їl~јсЩЅbqДтB!ј‘тR!Ф(+žxжшpєŽ;D~Cў?N8щ„щg\pП@оЫS J8ОCб)в:Г•тPž_џь^yj9щ&›E—,ЂaB ЈWOІ.cй ћћЗюз§;њŸ{7З,XpЬ#RŠX,уЉш˜аcћі-єѕѕ†šL&MmmЃšE­ А,­-b19"+яМуvuwѓЕЋЏЦw]œR‰^q%;6mФВЌœєќхJŠёГ›ЙќšЫi›аF9(уJ—@єіглз‹SvМЏф!,А#‰*}9rэУ@ХKX ™nО lќ3СŽЗ]zщЅБ… A…JJиžK хч*5МХВKј(%YўЬ ­Т`Y‚˜m‘ТВ д Ѕ"”E^zёEтё8mЕaл1Š…!Жn}›0 Ще B„€ИmCё:TќшЖ[8ћ’K0кр”J<~Я=xЅs˜GM} лЇqЬL9• Ђр(œРaџр~vvяЄX("}IyАЬ`з  mЌЈ@ъˆ6…б†|Ož]ЧbБ臕R~ м <1œ†ў‘vЩИqу:ЏМђJ” q=RЉI>Я%фˆžUˆ еH€+K()U_C}m5ЎчEйІqПяћджVсz’˜-xэ•зI$Ьžw8…Rфё Эеtuu“He№…D ƒ•4$т6ЙLŠgŸYNККŠЮysqJ%мr‰7ž_СЏј< Ž™Д0–AEoЙ?є Т€@єхћиМs3Cљ!|ЯGњ’Р Ђ$,KX„AH ”­*TЇ œgаЁoW={ž+–?SJ­<іьЧд5V-yўЁ5Kњї]рYрРЃРю?vИќкkЏ%•JсК.NйЁT.уКиЁ GдDєh+§,ŠHP[“ЃЊК–T:ЌhlEmm‘]Лv“NЅpНщ(—ІuNgА B Z–ЃˆKс ŽСšX:FU6E.“р‰ЧхЈSNСw=|Чaѕ‹/Q[WЭФШљ(ЙB"Е$а2”(­(–ŠьйЛ‡BЉ0тБЁ ‰Ѕbфs іЂЅЦТТ„HЪeЪeќ’пэ;ўЫA9XnŒљБ;ф1ржзžоrЬхї€3>џaz6—гo>Нуф•Ы7ŸМc§ОcVWРџ)Аіў‰)SІ4žsЮ9H)q]rй‰ВЧ@*EЈ+€Б‘4]“/R’ˆ'•Й)*дKE””Фт Є )‡Ђšn*Gб‰Їж†PE€ ;‰/l!Б„ —IP“KБ{зvzћz™1žуј>kWЎdўёѓ)†%Д"E€4рЁQZсљC…!МРУш(ъJ!Э`Ау6™К ZщшF!„0А{р…}…‹м!wџяЋр-+ zч}яSOžvЪ•­ДЖ5ГрЦАфтNмФъчvЭzљЩMГVПАэ_”дЏ?ЪРЇ>їЙЯпp‡Вырћ/‡УŠAXЊЖС08GЩ€D"yў№ё*ЄX, ”"O"УpЄ†Ђ”D*†Tч’ькК—x<†T eˆ†˜Hd"ЯN&l^|сežеЏпЇЏЇ‡ОžnІЭ_JA !‘H”QhЃ uˆT‘t uHTЏ”ˆEЄŽ„˜JЎdХ,’ё$Єapї міћР\єnѓяЛя лO™sсš“Ё:[ЧЈšf&пЬag.Bmёмц§ђ'oќxџюќMРЪl6;yщвЅ(%q]'Њ‘K”ЫN%PЈ…Dйс†!C…aЈˆЧтRF2JG%ьЁќ `ˆ'’xR“ЮTGЕч=лi9h2ЁБ)єя&rЙ…r€T!‰m$ьё˜ жЌYУсЇ,Aњ>ЁRlZЕŠŽЉ$jт8Ёƒ4J{јњдpёL)Дв˜ааПНПр“ЉЮKХАbЁ q†іoлoђнљы“щф#ЈF P>шхЭЅ/ў ИzЯ*=§œ2ћЛйOUРobж™c8і#чаЕΘЛуыЯŸяH$’•ЂTў~њqЪRЉŠDŠtiЈЄфУtАјјcp=F)m"^Жm‹QЃF‘H$IІв8~€ВRtŒЯŽэлщя‰x<ЮЬYГилгM:“ЁXіС„`\(„ ЅOяО}єіі2vТd У­kзrєщGPVeк!†ЪџЏuDJ!}‰яњјЎ ЛиёњŽwОŽ`Šmл ТcLA+Н^+}Пєф›Х? 9~[‡kрz Пиѕ}{яzsј!Еiьt№‡оСn,ЕЁѕ9Ъ]qzЛ‡(ч}–.=іŽкЧЕгўеЏ*…є””ш0)ГэиД‘п<ё$?ЙчnкЦЖ1kж!дезHIjвй\Ѕец†ƒЮHХЈђ™ж!Ё’СpШХж€,PJc`4{vэЦВТJ`‰€ИeЕФїЅ’eіюнЫД‡нm62zJayАQЬУž­CђЛиНfЗ[ш)\­ЄКЁИПј7Їп)Е˜лЗ™Л„0KŽНтtТ0Фs<ЧСwНt"„`TS3gђ“,њаY<Бl?ё3gЮbіьЙ$ b‰ТВ1ˆпл R… |Я‹К@Ўu‚<\*‡’НН}A4’’L&hm=ˆ=]]ЄRY’™в2јŽC ЦаппЯС3gbДfh ŸТРЭc‘„2Оuр‰’gШЁW?ћЖюѓJНЅ{'ИІњіїЧ‘Rbg2\xйežЧкW_х•ч–гЙd*‰l‚x2F(#…1д3Dџž~Sю/ЏђKў2ЈЛн!wя{бs|чFjѕ(ЫјЅT†w[–uV&—D‡‚ъКzк'Oaтєщј>Жc6ЩTŠt.‡eл<§аƒЌZё',>‘E‹O&д†ВусИ•~ІытЙ>ž ‰ЪЂ; 1•Д_eYXЖ‹EkИ­UсїPЉ•яыуС[nСи1тUЕXё$Н[ж2~A†‰sšxўЮ8љљЪРа{>ќ‡,<јсЏ‘m’У—С№R‘>)Щ›ї eHiП&зЅНšbЏ—пБqcRX–=ІлЖ#щWIdT }ŸР‹ъяуЛ.žур‹” JCЪ…NБ„ы”ё‡Рѓ*R1№<|зy-GЮщс;.В"Ÿь1ЃОc naС[Јn i?ё4ц-<ŽyGKг˜Б\xЩW к :М/У(п“Ц˜…Ц˜Œбe­u”дhЭЬ#л™vXc&жвиžЃЎ%EntœTЕE‹жњ:РжZћCѕXЈn$ЋСВЁыUУ™|aY|рŒ x{г:vmлLлјƒijiуЕ7V ZП/€ПКѕЕ>‚ wЪчэŽ:сT&LžUыFQ.Eвѕ ёгГg'эЇаадb ўЧљ†€Ыˆvч^fŒYcŒ1ЯiЅjTŠlmœdЮŽА4šmЁQhBœCЁЧАэYЭшмdОvУэeУaGРЙћ ™\nR%k|БГЦŠC,ЭЩe№† “ЇЯ&д†lЭhЦMœЪ‹/ў†]НЕЭуxхёЛ^у=к–јN€џћъхНс†gћ*^иЋш8xкJb*яEНЊІŽТа кРИ‰SЉд(~Ÿэn кЁћУС§E,А 1!Z+B]Га!aЈи№sЩ3WjV|6?rіG.fўQ'а>q Хќ Љt†Гч <тљЭЫ7)њЗД;yљж]{йд]dЭЎ!5M<џЋ_pпэ7Бgл&tЈО œ№~щ№—Д2ђњ=ЗL[’MN>ЭBšX"E *Ф„Hіљ8ЪІьИьюw‰W†?nџфУ/>Кс3чхHД]9<rє^Бё Ÿю5\ыX–ХџљєљŽ^|/<ѓ8Їžћ1ЦЖOт•OMЊt­NмfЎсzuОe“UTдЭпў2ѕc:0ЦАc§kИХСлжММЖRРКіЯйšђn–gя2†ѕы+пБэ%бiДaнњѕДoпG"•ЅЏр388ФњmньиЕ›юosѕЇNeпЎ-xў8џ3љОђЏoЛrљqНfсoyія.іJж<фqѕ З1mж<>ї/пцІo]ЮќcNbЮ‘'rя­пcѓЮtЂjxя@ј№92!(в$іn_?oяіѕе•cжm1TќьOй'кФz10ЧВcЂ~L;WІаз3|LXћ9АињGžЛXqшЩЇ§s'Б*Eй-Rі ”н"Џмг {fqнЭŒhvŒсѓџx9ŠЃЮј$пМшXNњиь\џЏ>yїrрќ кŸЛхd бЮкцJD_є§х„?ѓœЭРџЕlqFы”jЛЅ3Ыш 6ЉFЩŠџьчШЃ/cё‡>NмЖˆЧ,тЖ`љCwђЫGрќ+ятПnќ,Л6Ќ$”С№сЪџџ№ївZEБШРD ;Ўs3Ž:š†’Љ4Zљl}уY^|євЙZœТ€2Ц\UХтяіХ™9Р€ѕ™LFY–e’ЩЄV—‹‰ўвФпМ§7Tn[СЌUЊЦIENDЎB`‚tortoisehg-4.5.2/icons/24x24/0000755000175000017500000000000013251112740016415 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/24x24/actions/0000755000175000017500000000000013251112740020055 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/24x24/actions/hg-pull.png0000644000175000017500000000211613150123225022131 0ustar sborhosborho00000000000000‰PNG  IHDRрw=јsRGBЎЮщbKGDџџџ НЇ“ pHYsУУpMBtIMEл'{г/ЮIDATHЧЭ•[LuЦ3ЛГГ , ЌrDЊFWRCЋ!Ѕб(}№е`Д‰ E411mblb|№іЂ‘Ф4ЦыЅ5оbк>PЋЉiƒщЭ4Dmjњ€ июХ]іТВз™9>PЗP@ЁѕСѓxцЬї§ПџљцИŒz`ЇVЙёCьs{'>BњэвYeЙ ]ЛДjхе2oёНЇV›LЄЬєLnЬ‡ЙНГЈјЗљf0˜Ÿ5ѓйЃ"МДО—ШВ:пеіT”Мwu]i™ЛДXёOLIиёFuVЏЈаэ.ЖЕuуѓV`Y.ЂЁsœ=йПЗ}3Јџўр€іЈЋDпw}cMЙeЪиЯ~П"rзўоLSfT|№аЖЌйТБёг &HЗGшцпу"р-En§sпъz"с™iЩ&І’]žЪ†Œqжжwp{e3}ˆ|fŒМс.<^RС§я`ЗkЖЯ|Ћы јƒh6]BФоœ  iАЂ`Р 2ЋфBtюrT‰H‘Щї§,Іjг6еЎђњ‚нСT0ЕФzsAЌ H|ўщФ(Ь\T кЃ JŸІkC]Ž:нхию­ђ("‚нІI4œм§хгFbŒ ’Ÿwтй^РY |2•CсЙЦЕ ыnЧHгšыš‚6›D$ ТЮKn1Žђіи™o‰FЩЄ!›-">Ѕ39–"cШ.АщЦїѕ#-m7м №)q—0v&0b™VЏ(J$v /Ј*A{ыEзtИtХ"щ,Ѓ™,GЖОЦЧЁђ‚у{ИaѓyБuђ–пVwc%ЧУL"'‰hR‰GRœьЫнВˆ/ўоЇБ y|7Ю’†Нз6?уЙЃІ…аp?ПЦGЉЎЉ"M’Iц”кhTз3­–0žБXS8Б‡c­ћлu{ёєyЖ эРiзXWyV5SЊЗђУС'~ZзУ­+Щ-ћьжiз4'(WФ№шХМмі5ХnTEApc~Dh^i0ОЩ #Eї€ту{ъJЃˆУ4Љ1-чхямЈ0РŠƒ’‡м)ШBŒ–‘BЬ4Ђ4\!˜`%@]"`ХИRГIˆ8ўSu€ЄAВ+qсВ.ќцЌ”‹yуX)ЄГ|3єƒВФЛŠЮtLШц89›Ч+л1щчyћw_дTžo.ЏiEзуhjйtžLJ%іgœpрї_&lђ+!(XІыNœ›юcCБ‹UЅ^U(Г,b–01т№'_qјрQвќпъ/щЈЕ˜яIENDЎB`‚tortoisehg-4.5.2/icons/24x24/actions/hg-push.png0000644000175000017500000000210313150123225022130 0ustar sborhosborho00000000000000‰PNG  IHDRрw=јsRGBЎЮщbKGDџџџ НЇ“ pHYsУУpMBtIMEл,яŽeУIDATHЧЭ•Mh\UЧї}O&Щ41Бкк€PЅM\Бh[ŠqЃ‹hу""ŠW](шNAЌ Ц…mЕ.ЄыЂжЏкJеˆвOLI›Є™|˜ЩЬ$ѓо›wп;.&“2]x—чўП{ЮНчс&­г‡БПџ {q\е$zцDxнvЗзпвО4ХЩп.ЦQxR„зЖvё—U @рРЦэoюYБr#*Й†H‘$йн‘щИдїі*р Ѓ€iY{2­wЂ”d††ŒiYXЕі^є boC•$ ЮDКЁКoд|ЛтC’ƒ8 ЩxЅqп@х$H~qYеœZ.€HДрФ•Xре(g'єп —НHрCж‘Ÿr(16Љ?Тšц ОЛg?Oy;UJuј л+ЩЯAШЗћоргёYЂ*рд!ж ьUЪиeкЉ;L+нGљœжс п(8Мe/#K}пЙ7JŽ(w&(>јеѓб…uъ ;Н†ѕGзЖПi^}І ($BЌcІВ—>џIС/ эйкХБљт=Ю‹^ѓ[ыяnuу“?‡Вwєv—ЯWЇёгцЮ/ЖИж8(’9Ѕп-–8qфkN;‰Яџm§ Д5Щ(‘IENDЎB`‚tortoisehg-4.5.2/icons/24x24/actions/hg-outgoing.png0000644000175000017500000000123013150123225023004 0ustar sborhosborho00000000000000‰PNG  IHDRрw=јsRGBЎЮщbKGDџџџ НЇ“ pHYsУУpMBtIMEл!ЈТ›нIDATHЧэ”ЯkAЧ?ГЛй4ЁЁ5ЕDЩEщСƒН Ђ 7/<‰(Šžq•›­Ш‘ГЯ№д(lЄ‚ˆAФ"b1єѓfт Ч.­So'х(зw?рm8’ЇбPŽкБжГESHъ$x9ˆ^‚љŽФ?‰ѕКњЫZqіК€Ф`—A ш)ˆ^!І„5UЂВ-YјЗ~Ф š–)†V68оЦщ‘„ИžCWЖ­#ТZФг‹Ёк4dБkЌ‚DM6жпвЎЄЛtїЅH$н†ОAФnщ?!Fъ ў™Бd˜йечyшыDkM9Јamc›ЦQ…ЏŸ:šМ–N‚†Јg=пЙ‹ыC)Ј07S$XЈВКbФŸПqP9Я?.~yOЯОЃј‰2Ўrбa’А&…"Ѕ ТЬ,§€yrЁ648ъп^.…L\‹РрЈ/Mџ"Рy4Ф@o–S™4w•˜ч‰П~ŸщЕAЕWќYVџŒм‚IENDЎB`‚tortoisehg-4.5.2/icons/24x24/actions/hg-incoming.png0000644000175000017500000000071613150123225022764 0ustar sborhosborho00000000000000‰PNG  IHDRрw=јsRGBЎЮщbKGDџџџ НЇ“ pHYsУУpMBtIMEл23ю6€дNIDATHЧэ•НJA…ПIТJЪmmDЂиЕк<‚ёl”Tж6F KA%ЕFK!…" "‹ |! 1И“§БˆЛ,СЦЭFrš™9w8wюЯЬР!Т)Шл>jGгЩrЮ+ЮEF wюЗWЗpК[ŒssЖІ#"AOЮ+N25C2•№И•г=lтиЦƒЧХ‚ŠЯ/Maš–ѕXyŽзЋа:‚"FуГлЬСdBХВlžnыбMEтЏ=ƒAD J‘e›Hi0З0СОT(Н\"""œ.ъ/ЌчЫгHйФЖкм_ЗаtФакДV'ЗЖЫ[иї*ЮŸт$ƒZ.р|…эЅУ`нжŸGЏxЮћБњ§*ЧьРтaьћ}Œ"E№O"№= Y—kдую:ЈИїсhzяo0-„+V­мuкЦќіŸтt–њzIы:IENDЎB`‚tortoisehg-4.5.2/icons/shelve.ico0000644000175000017500000001246613150123225017623 0ustar sborhosborho00000000000000 h&  ЈŽ(  5VVVVVпџџџџџŒ—џџБž>‡J €Д”}џЖ—€џЖ—€џЖ—€џЖ—€џВ•€џ/-,џџМ‡J €шшцџьююџьююџьююџьююџьююџвддџџџŒ‡J €шшцџьююџьююџьююџьююџьююџьююџ•ƒwџњџX‡J €шшцџыээџыээџыээџыээџœџijjџQC9џзџє*‡J €шшцџтффџрутџрутџрутџ000џџџџџџq‡J €шшцџзлйџдйзџдйзџдйзџЄЈЇџ‰‹‹џo\Nџddc‡J €шшцџЬбЯџШЮЬџШЮЬџШЮЬџШЮЬџосрџЖ—€џ‡J €шшцџСЧХџМУРџМУРџИЙГџŸ~eџЋ‹sџ˜fCњ‡J €шшцџимлџжкйџжкйџШПЖџДЂџЇŠqњ‡J Q‡J €хтрџщщшџщщшџщщшџжЫТџ’`<љ‡J O‡J @‡J €‡J €‡J €‡J €‡J €‡J Dџџџџўў€€€€€€?€?€?€€џСџџџ( @ 2ЂЌЌЌЌЌЌЌЌЌЋdИџџџџџџџџџџџќЧџџџџџџџџџџџџ ŠџџџџџџџџџџџфЬџџџџŠ<<<<< ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џwAџ џџџџё(‡J џоивџцфсџцфсџцфсџцфсџцфсџцфсџцфсџцфсџцфсџцфсџцфсџЌЌЊџџџџџз‡J џфсоџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџ„……џџџџџА‡J џфсоџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџTUUџџџџџ~‡J џфсоџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџнмйџџџџџ§L‡J џфсоџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџцфсџvAџыџџџ№&‡J џфсоџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџоррџвддџвддџЬЪШџxBџ_ўџџџе ‡J џфсоџьююџъььџъььџъььџъььџъььџъььџъььџъььџ ЂЂџџџџџџџџџџџџ‡J џфсоџьююџхшчџфццџфццџфццџфццџфццџфццџфццџ]^^џџџџџџџџџџџџх‡J џфсоџьююџрутџоррџоррџоррџоррџоррџоррџоррџabbџџџџџџџџџџџџо‡J џфсоџьююџлпоџимкџимкџимкџимкџимкџимкџимкџДЗЖџ;<<џ000џ444џ221џџШШШШШУY‡J џфсоџьююџжкйџвждџвждџвждџвждџвждџвждџвждџвждџвждџжкйџьююџцфсџ‡J џ‡J џфсоџьююџвждџЬаЮџЬаЮџЬаЮџЬаЮџЬаЮџЬаЮџЬаЮџЬаЮџЬаЮџвждџьююџцфсџ‡J џ‡J џфсоџьююџЭваџЦЬЪџЦЬЪџЦЬЪџЦЬЪџЦЬЪџЦЬЪџЦЬЪџЦЬЪџЦЬЪџЭваџьююџцфсџ‡J џ‡J џфсоџьююџШЭЫџРЦФџРЦФџРЦФџРЦФџРЦФџРЦФџПХТџАЃ–џЏЁ“џЕІ˜џЮНАџЪЖЈџ‡J џ‡J џфсоџьююџУЩЧџКРОџКРОџКРОџКРОџКРОџКРОџЈ˜ˆџŠP(џ”dBџ”dBџ”d@џ‡J џ‡J ъ‡J џфсоџьююџЩЮЬџСЧФџСЧФџСЧФџСЧФџСЧФџСЧФџЋ˜†џ™mLџЯзгџЮжвџЃfџ‡J ь‡J!-‡J џфсоџьююџьююџьююџьююџьююџьююџьююџьююџЦА џ™mLџЮжвџЂ€eџ‡J ъˆJ ,‡J џфсоџьююџьююџьююџьююџьююџьююџьююџьююџЦА џ˜lKџЂcџ‡J ъ†K+‡J џоивџцфсџцфсџцфсџцфсџцфсџцфсџцфсџцфсџТЊ˜џˆK!џ‡J ш‡J *‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J ц‡J (џџџџџџџџџџџџџў?џќџќџќџўџрџрџрџрџрџрр?рррр?рџрџрџрџрџрџр?џрџрџџрџџџџџџџџџџџџџџtortoisehg-4.5.2/icons/detect_rename.ico0000644000175000017500000001246613150123225021134 0ustar sborhosborho00000000000000 h&  ЈŽ(  џ€P €P €P €P €P €P €P 75.s6YBе6bFу680ŽM;'ŠJ ”`<џЋ…jџЋ…jџЋ…jџЋ…jџЋ…jџ”r[џ6‹\џ4тŠџ4тŠџ5ЈlџA;.nŠJ ­ˆmџьююџьююџьююџьююџьююџмнмџ@nRџ4Фzџ4Я€џ7|TџR>*\ŠJ ­ˆmџьююџьююџьююџьююџьююџьююџвдвџ’”џ‰Š†џŸˆwќˆJ <KKFљ^_[џ^_[џ^_[џ––“џьююџьююџрттџСТСџСТСџСТСџЅŽ~ќˆJ <KKFљ^_[џ^_[џ^_[џ“”‘џшъъџшъъџМНМџ8rPџ7›fџ7œfџMM:ўˆJ <ŠJ ­ˆmџьююџотсџосрџосрџосрџЫЮЭџ7zTџ4тŠџ4тŠџ@aEўƒI!>KKFљ^_[џ^_[џ^_[џ‰Š‡џдижџдижџдижџŠŒˆџ6šeџ4тŠџ5Гqџ:?3Ќ<--KKFљ^_[џ^_[џ^_[џƒ…џТШЦџТШЦџТШЦџШЭЫџy|xџ5Іkџ4тŠџ4Дqў6>3Ѓ333ŠJ ­ˆmџьююџТШЦџОХТџ:N>џ5~Uџ5sOџxnbџЉ”‚џaWGџ4Ц{џ4тŠџ5—bў670JŠJ ­ˆmџьююџТШЦџРЦУџLmXџ4тŠџ4р‰џJP=џЏЃ”џ}l\џ5Іjџ4тŠџ4Фzџ670yŠJ ­ˆmџьююџьююџьююџuzsџ4ж„џ4тŠџ5Тyџ6zSџ6Š[џ4к†џ4тŠџ4Їk§670TŠJ  sTџЪИЊџЪИЊџЪИЊџЎœџ9vRџ4р‰џ4тŠџ4тŠџ4тŠџ4тŠџ4еƒџ6S>м55,ˆD"‡J T‡J T‡J T‡J T‡J TO=*p6U?й5‘_ќ4Їk§4Јl§5†Z§6G8М66/#33364-.75/L56/O55,%џџџЧРРР€€Р€€РРРРџџџ( @ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ44/664.„6:1й690ю560Є74.T... џџџџџџџџџџџџџџџџџџџџџџџџџџџџ€P €P €P €P €P €P €P €P €P €P €P €P €P €P 98.–64.џ6T?ј5]џ5žfџ6jJћ691ћ691оM;'џџџџџџџџџџџџџџџџџџџџџџџџŠJ ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џtE$џ76.џ4Йtџ4тŠџ4тŠџ4тŠџ4тŠџ4и…џ8P=џI;,ЪџџџџџџџџџџџџџџџџџџџџџџџџŠJ ‡J џНЂџЯРДџЯРДџЯРДџЯРДџЯРДџЯРДџЯРДџЯРДџЯРДџЯРДџЯРДџ„{sџ:ZEџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ5˜cџ;;0юџџџџџџџџџџџџџџџџџџџџџџџџŠJ ‡J џгХКџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџЏАЎџ:?6џ4д‚џ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ6mLџB;.иџџџџџџџџџџџџџџџџџџџџџџџџŠJ ‡J џгХКџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџчщшџX[Tџ;L=џ5‹\џ4Тyџ4еƒџ5Ђhџ9hKџ87/џhB%—џџџџџџџџџџџџџџџџџџџџџџџџŠJ ‡J џгХКџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџпсрџ‘‘ŽџRUNџ64.џ64.џ@B:џz{wџV8љˆJ xџџџџџџџџџџџџџџџџџџџџџџџџŠJ ‡J џгХКџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџезжџФХФџъььџьююџ”_;јˆJ xџџџџџџџџџџџџџџџџџџџџџ871ш64.џ64.џ64.џ64.џ64.џ64.џ64.џHGBџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџ”_;јˆJ xџџџџџџџџџџџџџџџџџџџџџ64.џ…Šˆџ…Šˆџ…Šˆџ…Šˆџ…Šˆџ…Šˆџ…Šˆџ64.џьююџьююџьююџьююџьююџьююџННМџ––”џ––”џ––”џ––”џ––”џ––”џ––”џ|T7њˆJ xџџџџџџџџџџџџџџџџџџџџџ64.џ…Šˆџ…Šˆџ…Šˆџ…Šˆџ…Šˆџ…Šˆџ…Šˆџ64.џъььџъььџъььџъььџъььџъььџŠџ8<3џ:TAџ:TAџ:TAџ:UBџ:UBџ8=4џaH4ќˆJ xџџџџџџџџџџџџџџџџџџџџџ871ш64.џ64.џ64.џ64.џ64.џ64.џ64.џHGBџхшшџхшшџхшшџхшшџхшшџхшшџ’“џ;XCџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ:eJџaH4ќˆJ xџџџџџџџџџџџџџџџџџџџџџџџџŠJ ‡J џгХКџьююџьююџтхфџрууџрууџрууџрууџрууџрууџрууџрууџ Ёžџ9@6џ4оˆџ4тŠџ4тŠџ4тŠџ4тŠџ:hKџYE3ќˆJ xџџџџџџџџџџџџџџџџџџџџџџџџŠJ ‡J џгХКџьююџьююџнсрџлпоџлпоџлпоџлпоџлпоџлпоџлпоџлпоџвжеџ;<5џ5_џ4тŠџ4тŠџ4тŠџ4тŠџ5Ÿgџ980џH"џџџџџџџџџџџџџџџџџџџџџ871ш64.џ64.џ64.џ64.џ64.џ64.џ64.џFFAџжкйџжкйџжкйџжкйџжкйџжкйџжкйџ–™•џ76/џ5Ѓiџ4тŠџ4тŠџ4тŠџ4н‡џ8E6џB;.м... џџџџџџџџџџџџџџџџџџ64.џ…Šˆџ…Šˆџ…Šˆџ…Šˆџ…Šˆџ…Šˆџ…Šˆџ64.џбждџбждџбждџбждџбждџбждџбждџбждџˆ‰†џ871џ5Ўoџ4тŠџ4тŠџ4тŠџ4Щ}џ7G8ќ591Э<--џџџџџџџџџџџџџџџ64.џ…Šˆџ…Šˆџ…Šˆџ…Šˆџ…Šˆџ…Šˆџ…Šˆџ64.џЬбЯџЬбЯџЬбЯџЬбЯџЬбЯџЬбЯџЬбЯџЬбЯџЬбЯџy{wџ792џ4Иtџ4тŠџ4тŠџ4тŠџ4а€џ5L:љ690Ы333 џџџџџџџџџџџџ871ш64.џ64.џ64.џ64.џ64.џ64.џ64.џED?џХЫЩџЙПМџЙПМџЙПМџЙПМџЙПМџЙПМџПХУџЧЭЫџЧЭЫџmpkџ9>5џ4Пxџ4тŠџ4тŠџ4тŠџ4Ю€џ6E7њ691Н333џџџџџџџџџџџџŠJ ‡J џгХКџьююџьююџЧЭЫџТШЦџТШЦџОФТџ860џ64.џ64.џ64.џ64.џ64.џWYSџТШЦџТШЦџРЦФџrtoџ:B7џ4аџ4тŠџ4тŠџ4тŠџ4Щ}џ691ћ74/bџџџџџџџџџџџџŠJ ‡J џгХКџьююџьююџУЩЦџНФСџНФСџНФСџDF?џ5‡Zџ4Ч|џ4Ч|џ4Ч|џ5œeџ870џ_=џ‘a>џ‘a>џ–fCџC?3џ5‚Wџ4тŠџ4тŠџ4тŠџ4тŠџ5wQў691ХџџџџџџџџџџџџŠJ ‡J џгХКџьююџьююџОХТџИПМџИПМџИПМџUYRџ5Wџ4тŠџ4тŠџ4тŠџ4и…џ75/џzZ@џІ‡nџІ‡nџІ‡nџfQ@џ8Q=џ4тŠџ4тŠџ4тŠџ4тŠџ5 gџ681ыџџџџџџџџџџџџŠJ ‡J џгХКџьююџьююџЫаЯџЧЬЪџЧЬЪџЧЬЪџpqmџ8iKџ4тŠџ4тŠџ4тŠџ4тŠџ7qOџ>?5џЅЊІџЫгЯџІ’џB=0џ5…Yџ4тŠџ4тŠџ4тŠџ4тŠџ5Њlџ66/љџџџџџџџџџџџџŠJ ‡J џгХКџьююџьююџьююџьююџьююџьююџ˜˜•џј64.…џџџџџџџџџџџџŠJ ‡J џгХКџьююџьююџьююџьююџьююџьююџьююџ‚~џ:J<џ4л†џ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ5žfџ64.ў55,:џџџџџџџџџџџџŠJ ‡J џЁtUџЉ‚fџЉ‚fџЉ‚fџЉ‚fџЉ‚fџЉ‚fџЉ‚fџЂ}cџ?=4џ6uPџ4й…џ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ5­nџ65.ў75.tџџџџџџџџџџџџџџџˆD"‡J Ј‡J Ј‡J Ј‡J Ј‡J Ј‡J Ј‡J Ј‡J Ј‡J Ј‡J ЈjB$Р:9/ј8?4§5–bџ4оˆџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4Мvџ5iJћ64.џ66/џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ999 64.u68/ђ57/§6I9і5_Eј5wQў5|Tџ5`Eј6D7ї64.џ691М55,:џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ33355.M74-k65/ˆ75/Ј580Б64.Š55-e66+/џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ№џџџР?№№№№№?№?Р?Р?Р?Р?№?№?РРРР№№№№№№№№№џџ€џџ№џџџџџtortoisehg-4.5.2/icons/proxy.ico0000644000175000017500000001525613150123225017516 0ustar sborhosborho00000000000000  Ј6hо hF( @ 64. 64.Z64.ž64.а64.ј64.ў64.ў64.ї64.Я64.64.X64. 64.64.Ž64.ѓ@<0џUN3џe\6џ`b:џth8џsh8џod7џe\6џUN3џ?<0џ64.ђ64.‹64.64.g64.№IE1џlb7џq:џq:џX~Gџ!Јcџ}p:џq:џq:џq:џq:џq:џka7џID1џ64.ю64.c64.64.Ї<9/џh^6џq:џq:џq:џvp<џБjџНqџtn;џq:џq:џq:џq:џq:џq:џq:џg]6џ;9/џ64.Ђ64.64.64.МC?0џwk9џq:џq;џr;џr;џ=”YџТxџМsџvn;џq:џq:џq:џq:џq:џq:џq:џq:џvj9џB?0џ64.З64.64.ЈD@1џ|o;џt=џt>џ‚u>џ‚u?џjxEџПyџУ|џ!Џnџr<џ€r;џq:џq:џq:џq:џq:џq:џq:џq:џzm9џB?0џ64.Ђ64.j<:0џzo=џƒv@џ„wBџ…xBџ…yCџ„xCџ4ЂiџФџУџ3ЂhџƒwAџ‚v?џt>џ€s<џr;џq:џq:џq:џq:џq:џq:џfn=џ2B4џ64.c64.64.ёlc<џ…yCџ†zEџ‡{Fџˆ|Gџˆ}Hџc…RџХ€џЦ€џЦ€џ@eџ†{Eџ…yDџ„xBџƒv@џt>џ€r<џq:џq:џq:џq:џq:џLˆMџ“[џ64.ю64.64.’LH4џ‡{Fџˆ|Hџ‰~IџŠKџ‹€LџˆLџ.АsџШ‚џШ‚џШџ&Зxџ~|Jџˆ}Hџ‡{Fџ†zDџ„xBџ‚v@џ€s=џq;џq:џq:џ~p:џ%ІaџРpџ4K8џ64.‹64. 64.іtkBџ‰~Jџ‹€LџŒNџƒOџŽ„Pџb[џ!Ъƒџ!Ъƒџ!Ъ‚џ Ъ‚џ Щ‚џ7Љmџ†~LџŠKџˆ}Hџ‡{Fџ…yCџƒv@џt=џr;џq:џS€IџТqџТqџ)…Rџ64.ђ64. 64._B?2џ‹KџŒ‚NџŽƒPџ…Rџ†Tџ…Tџ0Еuџ"Ь„џ"Ь„џ"Ь„џ"Ьƒџ"Ыƒџ!Ыƒџ_\џ‚Nџ‹€Lџ‰~Jџ‡|Gџ…yDџv@џiwDџFUџМrџТrџТqџДjџ:;0џ64.X64.Ѓ\W<џƒOџ…Rџ‡Tџ’‰Vџ“ŠXџh’`џ$Ю…џ$Ю…џ$Ю…џ$Ю„џ$Ю„џ#Э„џ#Ь„џj[џ†RџŽƒPџŒMџŠ~JџoMџ0ЇlџТ}џУ{џТxџТuџТrџТqџ1]@џ64.64.жqjEџ…Rџ‘ˆUџ“ŠXџ•ŒZџ–Ž\џf_џKЉoџ0Лzџ&а…џ&а†џ&а…џ%Я…џAЉnџ‘‰Xџ’‰Vџ†TџŽ„Pџj†Tџ"О|џХ€џФџУ}џУzџТwџТtџТqџ$„Sџ64.Я64.ћ}vKџ’ˆVџ”‹Yџ–\џ—^џn”cџr’bџ›”dџš’cџ~’cџ\ЃlџJЌqџfšgџ•Ž^џ–Ž\џ”ŒZџ’‰WџŽ„Sџ+ДvџШ‚џЦџХ€џУџУ|џУxџТuџТrџ!”Zџ64.ї64.џ†~Qџ”‹Xџ–\џ”Ž^џPЄlџ)в†џ_Єnџž˜hџŸ˜iџŸ˜iџŸ˜iџž—gџœ•eџ›“cџ™‘`џ—Ž]џ”ŒZџ‘ˆVџ3Џrџ Щ‚џШџЦџФ€џУ}џУzџТwџТsџ*Wџ64.ў64.џˆ€Sџ•[џ—^џLЇnџ)д‡џ*еˆџSЎsџЁ›mџЂœnџЂœnџЂœmџ šlџŸ˜iџ–gџ›”dџ™‘`џ–Ž\џ”‹Yџ~‡Vџ-Жvџ Щ‚џЧџХ€џУџУ{џТxџТuџ4ˆRџ64.ў64.ћzQџ—Ž]џl–dџ)д‡џ*жˆџ+з‰џHЖxџЄŸqџЅ rџІ sџЅŸrџЄžpџЂ›mџŸ™jџ–fџš“bџ˜_џ•Œ[џ’‰Wџ†ƒRџJ›dџ ФџЦ€џФ€џУ|џУyџТvџIrEџ64.ј64.зwqMџ˜_џ=Дuџ*еˆџ+з‰џ,й‰џ:П|џЇЂuџЈЄwџЉЄwџЈЃvџІЁsџЄžpџЁ›lџž—hџtŽ`џQІmџFЊoџa”`џ…SџƒPџQ’]џЦџФ€џУ}џУzџДoџ][7џ64.а64.Єb]Cџ‘Œ]џ,Шџ+жˆџ,и‰џ-кŠџ0зˆџ‰ rџЌЇ{џЌЈ|џЊІyџЈЃvџЅ rџЂœnџŸ™jџ—‘cџKЉoџ&а…џ$Ю„џiŒZџŽ„Pџ‰Lџ; gџФ€џУ~џЛvџZ}HџTN3џ64.ž64.`EB5џ…Š\џ*д‡џ+зˆџ,й‰џ.лŠџ0м‹џEС~џЉЇ|џЎЊџЋЇ{џЈЄwџІ sџЂnџ ™jџ–fџˆaџ0П|џ$Ю…џw…UџŽ„Pџ‹€Lџ€{Iџ.Іlџ8dџavDџ8–[џ2H8џ64.Z64.64.їfxSџ*еˆџ+зˆџ,йŠџ.лŠџ0н‹џ1пŒџrЉvџЌЈ|џЊІyџЈЃvџЅ rџЂœnџŸ™jџœ•fџ˜‘aџ<Бtџ$Ю…џZ–aџˆPџ\‹YџyGџvvEџMXџ"БpџЁfџ64.ѓ64. 64.•œdџКxџ'ˆZџ64.№64.64.n6>4џ.Мzџ+жˆџ,и‰џ-йŠџ-кŠџ.кŠџ.кŠџ-кŠџ,й‰џ@БtџІЃˆџЄ †џ˜‘`џ–Ž\џUžhџ#Э„џ"Ьƒџ Ъ‚џШ‚џЦџФ€џ!Љnџ6:1џ64.g64.Ќ6L;џ,Цџ*жˆџ+з‰џ,и‰џ,и‰џ,и‰џ,и‰џ+з‰џeiџЯаЫџЗЕЃџ—Ž]џ“ŠYџ1ЏsџJŸgџ3Бsџ Щ‚џЧџХ€џЖxџ4F8џ64.Ї64. 64.С5O=џ*Хџ*еˆџ*жˆџ*жˆџ*жˆџ*жˆџ8МyџŠ^џстсџийжџ‘‰Zџ‘‡Vџ_…Wџr†TџBЁhџШ‚џЦџЕwџ2L<џ64.М64.64. 64.Ќ5E8џ,Ћpџ(д‡џ(д‡џ)в†џ;Еvџz‘`џ‘‰_џхццџЦЦОџ‡\џ†TџhŒYџ-Жvџ"С}џЧџ$žjџ4B6џ64.Ј64.64.n64.ѓ4\DџTWџ€ˆYџ”‹Zџ”‹Zџ‡Yџ’Šcџ‡Uџ…RџŽƒPџy~Nџ!Х€џ(œhџ0]Eџ64.ё64.j64.64.•64.їDB5џ`[AџtnIџ~wMџ„|Nџƒ{Lџ{sHџphCџ[V;џ9?3џ64.і64.’64.64.64.`64.Є64.з64.ћ64.џ64.џ64.ћ64.ж64.Ѓ64._64. џџџџџџџџџрџџџўџј№?ррР€€€€€€Ррр№?јўџџџџрџ( kmo!l#x s"~ >o>5z7 ‰Ž Ž— *‰0š—Ÿ nxn,“5  Ѓ&™1MŒTww.š8ІЃ(ЋЌ$Є2-Ѓ:Ї2А" ГЏ)Г Ў/В'А-Г)А1!Ў7Ж'И# Б6"Б8IІSИ2$И;Р1LБSWЎ\^Ќb.ЙI"Т;2МKХ9&ТALКY5ПP,ХI"Ъ<0ХM&ЬB|В‚9ЦY3ЩQ>ЧU-ЯK:ЮZ[Цq6вW`ШpmЦyУ†>еaEзj:м^=нcVиvFнl>тdXлzeк‚JсtFцrйJщtу—QяUюƒTђ‚‡хЂЂхДyяЁcѕ•bњ–kћ}јІjџ ИюЧ†џБ–џОНџиiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii/iiiiiiii0>)@2,iiiii7EN? HH@&iiiDHRX1 -XR-iiiHR\[(,+LU* iiWOZM6 'Ua' iiWVF#8TcbL$iiYO!9HD^hfd: iii="@NVGegd= iii%"2тdџ Б6џlџCТY Ќ7Z1вQџBсiџTђ‚џUюƒџГ)џН&џЖ'џ>дaџKрtџ!­7џ ‰џnџXпb*СF™=нcџQяџEзjџ.ЙIџЇ2џА-џMпwџbњ–џБ.џŸ џŽ џxџ`ъf 2ЫQІHчrџ:ЮZџЏ)џЃ(џ2МKџeк‚џ}јІџkћџ>жbџГ џšџ} џeюd9д\Ђ>зaџА"џХ9џ3еUџ:ХWџЂхДџНџиџ†џБџjџ џ&ТAџ“ џ џeэ_?кd{,ЦIџ Гџ%Ъ@џ;м_џFцrџ[ЦqџИюЧџ–џОџlџЂџ.ШLџŸџ‹ џaсRBрi+­+њ ГџУ8џ5ПPџFнlџJрqџ‡хЂџyяЁџcѕ•џ3ЧQџІ*џџYЫ7 ‡ ЋЋџР.џ!Й9џ3ЩQџ<Ы[џXлzџ"Б8џЌ4џ9ЦYџ;б]џ ўBzt Ї*ЦЌџЙ1џ.МIџ0НMџ-ФJџФ0џЏ'џJуtџHщr§v Ž&А>І.Ѓ–ћІџЇџ Џџ Дџ  џ?гdр1ЊMg,ГG5 ‹‚“И$Д;Й Єs!s5џџџџ№рР€€€€€Ррјtortoisehg-4.5.2/icons/menucheckout.ico0000644000175000017500000001373613150123225021030 0ustar sborhosborho00000000000000  Ј6 hо  ˜F( @ џџџџџџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNЧšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџНgџНgџНgџНgџНgџНgџНgџНgџНgџМfџ ЃUєœOЛšN џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџНgџНgџНgџНgџОhџПiџРjџСkџПiџ ЁSєœO šNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџНgџНgџОhџРjџТlџФmџХnџФnџ ЄUєœP˜џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџНgџПiџТlџФnџЦpџШrџЩsџ ЈYѕPЏšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџПiџУlџЦoџШrџЫtџ!ЬvџГaћPШšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџТlџЦoџЩrџ!Ьuџ#Яxџ%бzџРmџQиšNџџџџџџџџџџџџџџџџџџ Ф< ФlЃЦ“ЇШН ЅШоЂХїЂХїЅШоЅШНЃЦ“ Фl Ф<џџџџџџџџџџџџšNџХnџШrџ!Ьuџ$Яxџ&в{џ)е~џ+з€џ ІXѓšN\џџџџџџџџџџџџ Ф,ЅЧг ЊЫђ6РкіgауџŠмыџzтђџ^чљџJхљџ@лёџ5бъџ+ШтџКзі ЃФђЄЧг Ф,џџџџџџšNџЧpџ Ыtџ#Яxџ&в{џ)ж~џ,йџ.л„џ'ЮyџQыšNџџџџџџџџџЄЧе,ЩтўWыќџ‹ђ§џПјўџЬљўџ˜ђ§џcыќџOщќџOщќџOщќџOщќџMфіџBУвџ#ЃИўЄЧеџџџџџџšNџШrџ­]џ$Яxџ(д}џ,иџ/м„џ2п‡џ4сŠџБaѕQ—џџџџџџџџџ ФџPыќџWыќџŠё§џОјўџЭљўџ™ђ§џdыќџOщќџOщќџOщќџOщќџMфіџBУвџ7ЃАџ ФџџџџџџџšNџСmџšNџГbљ*жџ.кƒџ1о‡џ5тŠџ8хџ6т‹џ ЁTєšNRџџџџџџ ФџPыќџVыќџŠё§џНїўџЭљўџ™ђ§џeыќџOщќџOщќџOщќџOщќџMфіџBУвџ7ЃАџ ФџџџџџџџšNџЎ^ј›OžP№#Ьvџ.л„џ2пˆџ6уŒџ:шџ=ы“џ/и‚џPѓšN"џџџ ФџPыќџUыќџ‰ё§џНїўџЮљўџšђ§џeыќџOщќџOщќџOщќџOщќџMфіџBФгџ7ЃАџ ФџџџџџџџšNџœPїšN#›O^ ЅWє.йƒџ2о‡џ5т‹џ8цŽџ:чџ8хџ УqќžQзšN ФџPыќџUыќџ‰ё§џМїўџЮљўџšђ§џfьќџOщќџOщќџOщќџOщќџMфіџBФгџ7ЃАџ ФџџџџџџџšNџPШџџџџџџžQШОk§0м…џ2пˆџ5тŠџ5т‹џ4сŠџ2п‡џЏ_ѕQЖ ТџPыќџTыќџˆё§џМїўџЯљўџ›ђ§џgьќџOщќџOщќџOщќџOщќџMфїџBФгџ7ЃАџ ФџџџџџџџšNџšNTџџџџџџšN#Pђ#Шtџ/м„џ0н†џ1о†џ0н†џ.л„џ+з€џЌ\ѕžnџNшїџSыќџ‡ё§џЛїўџаљўџ›ѓ§џgьќџOщќџOщќџOщќџOщќџMфїџBФгџ7ЃАџ Фџџџџџџџ›NQšNџџџџџџџџџšN<Pѕ#Ъuџ,йџ-й‚џ,иџ+з€џ)е~џ&бzџЌaџІdџBкжџ‡ё§џЛїўџањўџœѓ§џhьќџOщќџOщќџOщќџOщќџMфїџBФгџ7ЄБџ ФџџџџџџџџџџџџџџџџџџџџџџџџџšNKPі Чrџ(д}џ(д}џ'г|џ%бzџ#Яxџ ЬuџКgџ  Wџ-Ж„џ‚кЧџПђяџ‰чцџ`хюџOщќџOщќџOщќџOщќџMфїџBФгџ7ЄБџ ФџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNQœPђГbљ#Юwџ#Юxџ!ЭvџЫtџШrџХoџПjџА^џЅ[џšNџšNџ0͘џOщќџOщќџOщќџOщќџMфїџBФгџ7ЄБџ ФџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNPАŸRѓА_ћНiџФnџСkџМhџВ_џЉ]џ žTџBЗ‚џvмЯџiьћџOщќџOщќџOщќџOщќџMфїџBФгџ7ЄБџ ФџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšN5œOˆPн›OѕœUџ Ђ]џВ€џMЫБџЈяюџгњўџžѓ§џjьќџOщќџOщќџOщќџOщќџMфїџCФдџ7ЄБџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџPыќџ„ё§џИїўџгњўџŸѓ§џkьќџOщќџOщќџOщќџOщќџMфїџCФдџ7ЄБџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџPыќџƒё§џЗїўџдњўџ ѓ§џkьќџOщќџOщќџOщќџOщќџNхјџCФдџ7ЄБџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџPыќџƒё§џЖїўџдњўџ ѓ§џlьќџOщќџOщќџOщќџOщќџNхјџCФдџ7ЄБџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџPыќџ‚№§џЖїўџењўџЁѓ§џlьќџOщќџOщќџOщќџOщќџNхјџCФдџ8ЄВџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџPыќџAнёџRвшџXЪрџFОиџАаџЅЧџЄЧџЏЯџКзџ'Упџ1Ыхџ6Нбџ8ЄВџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФџЕдџЉЫџ6ПлџJЪуџ]еыџpрѓџƒыќџƒыќџpрѓџ]еыџJЪуџ4ПлџЈЩџЅУџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФЄЧџDЧсџˆюўџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџˆюўџDЧсџЄЧџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФЄЧвDЧсўˆюўџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџˆюўџDЧсўЄЧвџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ Ф$ ЅШЦ ЇЩѕ3НкєIЪуў]еыџpрѓџƒыќџƒыќџpрѓџ]еыџIЪуў3Нкє ЇЩѕ ЅШЦ Ф$џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ Ф0 Ф_ЂЦ‡ ІЩЕ ІШлЂЦіЂЦі ІШл ІЩЕЂЦ‡ Ф_ Ф0џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџРџџРџџРџџРџџРџџРџџР№Р€РРРРЬЬоџџ€џРџрџјџџџџџџџџџџџџџџџџџџ€џџ№џџџџџџџџ(  €џ€џ€џ€џ€џ€џ€џ€џ›2џ›2џ›2џ›2џ€џ€џ›2џК<џК<џ€џOБЧџ?ЁЦџ?ЂЧџ=ЁЦџ:›Фџ7•Тџ?ЁРџ€џК<џК<џК<џ€џTАЬџ™пџ4гѓџa№љџOріџ(ХэџІтџyхџ2˜Њџ€џК<џ$џmџК<џ€џ=†Кџ™пџ4гѓџa№љџOріџ(ХэџІтџyхџ'fГџ€џ›2џК<џ$џmџК<џ€џ=†КџBЦяџoшљџœјќџ№ћџaріџ<Я№џБђџ1Тџ€џ€џ$џmџ$џmџ$џmџ€џAШ№џpшљџžјќџŒ№ћџ^ріџ<Я№џБђџ1Тџ€џ€џ$џmџ$џmџ$џmџ€џ€џЁјќџŠ№ћџZріџ<Я№џБђџ1€Тџ€џ›2џК<џК<џ›2џК<џ€џWріџ<Я№џБђџ2}Пџ€џ€џ€џ€џ€џŠ№ћџXріџ<Я№џБђџ2}Пџ=ƒЖџ;Ш№џpщљџЁјќџ‹яћџYріџ<Я№џБђџ2}Пџ=Еџ;Ш№џpщљџЁјќџŒяћџZріџ<Я№џБђџ3|Оџ=„Жџœтџ5еѓџe№љџMпіџ%ХэџІтџyхџ3|Оџ;žХџ-ЫћџDељџЁјќџŒяћџZріџ<Я№џБђџ&mАџ=ЊПџ,ЩћџMиљџЁјќџŒяћџZріџ<Я№џБђџ=ЊПџOБЧџ2•Тџ5™Тџ4•Рџ/Нџ+ИџOБЧџ€џџƒ‚‚€ Ајќўўўўўџ(  џ† џ† џ† џ‚џ ’/ф… џЁ4џЊ7џ“џ˜7и'ЎL5­ЂП2ЋІП3ЋЊП3ЈЏП7ЋЗП9ЗО5Š џЕ:џК<џ‹ џ P’:ЊТю/ЖсџQиыџDЫшџ&Апџлџ-šЗѕŠ џ ЪIџйRџšџžWХ1’Ъџ5УэџfэјџVпѕџ,Рыџ—чџ#rРџ†ы›1ьзMџуYџГ>џ,ž—џ]кѕџ“єћџ„ьњџQйѓџ*Нёџ-ŠЫџ‰й—7ŒЁ5Ры\џ"јgџРIџ–=џ…хжџьњџNйѓџ*Нёџ-ŠЫџ˜<Ќ™'џЋ-џ(џІ+џ-ЉcџLйѓџ*Нёџ.‡ЩџœWБfџ-­zџKК}џ€ьњџMйѓџ*Нёџ.‡Щџ7ДЇ3<ЬџZлѕџ—ѕћџьњџMйѓџ*Нёџ.†Шџ;ИЊ3.–Ьџ,УяџhьјџTоѕџ*Рыџ—чџ({Уџ=ОЎ35Гиџ=бљџёћџ‚ьњџNйѓџ*Нёџ-“Уџ>ТБ ?КСЁ>ЌбџKЎаџFЉЮџ5œЩџ;ЅЭџ@КЧЈ№р№јјјјtortoisehg-4.5.2/icons/menuadd.ico0000644000175000017500000001373613150123225017753 0ustar sborhosborho00000000000000  Ј6 hо  ˜F( @ ‡J ‡J ‡J ‡J ‡J ‡J ‡J ‡J џ‡J џ‡J џ‡J џ‡J џ‡J џ‡J ‡J ‡J џФŒXўФ‰TџТ†MџО€Fџ‡J џ‡J ‡J ‡J џШ’_ўШ[ўХ‹UџТ†Mџ‡J џ‡J ‡J ‡J џЪ–eўЪ”aўШ[ўФ‰Tџ‡J џ‡J ‡J ‡J ‡J ‡J ‡J ‡J џЬ™kўЬ˜gўЪ”aўЦŽYў‡J џ‡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џТ†MџО€Fџ‡J џ‡J ‡J ‡J џмКœ§нЙš§лЕ”§йБ§ж­‡ўдЉўвЅzўаЁtўЮœmўЬ˜gўЪ”aўШ[ўХ‹UџТ†Mџ‡J џ‡J ‡J ‡J џоОЂ§пН §нЙš§лЕ”§йБ§ж­‡ўдЉўвЅzўаЁtўЮœmўЬ˜gўЪ”aўШ[ўФ‰Tџ‡J џ‡J ‡J ‡J џмМŸ§оОЂ§мКœ§кЖ–§иВ‘§йБ§ж­‡ўдЉўвЅzўЭžrўЬ™kўЪ–eўШ’_ўФŒXў‡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 ‡J џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџјџџјџџјџџјџџјџџџџџџџџџџџџџџјџџјџџјџџјџџјџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ(  ›iџ›iџ|Tџ|Tџ›iџй“џ›iџ|Tџ|Tџ|Tџ|TџџТHџ›iџ|Tџ|Tџ|Tџ›iџџТHџџТHџК~џ›iџ›iџ›iџ|Tџ›iџџкџџТHџџЖ$џК~џй“џй“џ|Tџ›iџ›iџ›iџџ…Hџй“џ|Tџ|Tџ|Tџ›iџџТHџџТHџ|Tџ›iџ|Tџ|Tџ|Tџџџџџџџџџќ?ќ?№№№№ќ?ќ?џџџџџџџџ(  ›iџ›iџ|Tџ|Tџ›iџй“џ›iџ|Tџ|Tџ|Tџ|TџџТHџ›iџ|Tџ|Tџ|Tџ›iџџТHџџТHџК~џ›iџ›iџ›iџ|Tџ›iџџкџџТHџџЖ$џК~џй“џй“џ|Tџ›iџ›iџ›iџџ…Hџй“џ|Tџ|Tџ|Tџ›iџџТHџџТHџ|Tџ›iџ|Tџ|Tџ|Tџџ№џ№№№№№Р0Р0Р0Р0№№№№џ№џ№tortoisehg-4.5.2/icons/menuclone.ico0000644000175000017500000001373613150123225020323 0ustar sborhosborho00000000000000  Ј6 hо  ˜F( @ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ Ф0 Ф_ЃЦˆЈЩЗ ІШмЂЦіЂЦїЅШнІЩЙЃЦ‰ Ф` Ф2џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ Ф$ЅШЦ ЈЪђ2НиѕeЯуўŠмыџ|уђџ`шњџLцњџAмђџ5бъџ*ЧтџЙеѕ ЃФђЅШЪ Ф(џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЅЧг*ЧсўVъћџ‹ё§џОјўџЭљўџ™ђ§џdыќџOщќџOщќџOщќџOщќџMфіџAТвџ#ЅЛџЄЧй ФџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЁХќPыќџVыќџŠё§џОјўџЭљўџ™ђ§џeыќџOщќџOщќџOщќџOщќџMфіџBУвџ7ЃАџЂЦљ ФџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЁХќPыќџVыќџ‰ё§џНїўџЮљўџšђ§џeьќџOщќџOщќџOщќџOщќџMфіџBУвџ7ЃАџЂЦј ФџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЁХќPыќџUыќџ‰ё§џНїўџЮљўџšђ§џfьќџOщќџOщќџOщќџOщќџMфіџBФгџ7ЃАџЂЦј ФџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЁХќPыќџTыќџˆё§џМїўџЯљўџ›ђ§џgьќџOщќџOщќџOщќџOщќџMфіџBФгџ7ЃАџЂЦј ФџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЁХќPыќџTыќџ‡ё§џМїўџаљўџ›ѓ§џgьќџOщќџOщќџOщќџOщќџMфїџBФгџ7ЃАџЂЦј Фџџџџџџџџџџџџџџџџџџџџџ У3 УfЃХЇШК ЅШоЂЦѕЂЦѕЄШоЁХўPыќџSыќџ‡ё§џЛїўџаљўџœѓ§џhьќџOщќџOщќџOщќџOщќџMфїџBФгџ7ЃАџЂЦј ФџџџџџџџџџџџџџџџЁУ&ЅЧЩ ЉЫё8Ркєmгцў‘рэџ‚цѕџaъћџNшћџDпєџЂХџPыќџSыќџ†ё§џКїўџбњўџѓ§џhьќџOщќџOщќџOщќџOщќџMфїџBФгџ7ЄБџЂЦј ФџџџџџџџџџџџџџџџЄЧг.ЬхўXыќџ‹ђ§џПјўџЬљўџ˜ђ§џcыќџOщќџOщќџЂЦџPыќџRыќџ†ё§џКїўџвњўџѓ§џiьќџOщќџOщќџOщќџOщќџMфїџBФгџ7ЄБџЂЦј ФџџџџџџџџџџџџџџџЂЦјPыќџWыќџ‹ђ§џПјўџЬљўџ˜ђ§џdыќџOщќџOщќџЂЦџPыќџQыќџ…ё§џЙїўџвњўџžѓ§џjьќџOщќџOщќџOщќџOщќџMфїџBФгџ7ЄБџЂЦј ФџџџџџџџџџџџџџџџЂХјPыќџVыќџŠё§џОјўџЭљўџ™ђ§џeыќџOщќџOщќџЂЦџPыќџQыќџ…ё§џИїўџгњўџŸѓ§џjьќџOщќџOщќџOщќџOщќџMфїџBФгџ7ЄБџЂЦј ФџџџџџџџџџџџџџџџЂХјPыќџVыќџ‰ё§џНїўџЮљўџ™ђ§џeыќџOщќџOщќџЂЦџPыќџPыќџ„ё§џИїўџгњўџŸѓ§џkьќџOщќџOщќџOщќџOщќџMфїџCФдџ7ЄБџЂЦј ФџџџџџџџџџџџџџџџЂХјPыќџUыќџ‰ё§џНїўџЮљўџšђ§џfьќџOщќџOщќџЂЦџPыќџPыќџ„ё§џЗїўџдњўџ ѓ§џkьќџOщќџOщќџOщќџOщќџMфїџCФдџ7ЄБџЂЦј ФџџџџџџџџџџџџџџџЂХјPыќџUыќџˆё§џМїўџЯљўџ›ђ§џfьќџOщќџOщќџЂЦџPыќџPыќџƒё§џЗїўџдњўџ ѓ§џlьќџOщќџOщќџOщќџOщќџNхјџCФдџ7ЄБџЂЦј ФџџџџџџџџџџџџџџџЂХјPыќџTыќџˆё§џМїўџаљўџ›ѓ§џgьќџOщќџOщќџЂЦџPыќџPыќџ‚ё§џЖїўџењўџЁѓ§џmьќџOщќџOщќџOщќџOщќџNхјџCФдџ7ЄБџЂЦј ФџџџџџџџџџџџџџџџЂХјPыќџSыќџ‡ё§џЛїўџаљўџœѓ§џhьќџOщќџOщќџЂЦџPыќџPыќџ‚№§џЖїўџжњўџЁѓ§џmэќџOщќџOщќџOщќџOщќџNхјџCФдџ8ЄВџЂЦј ФџџџџџџџџџџџџџџџЂХјPыќџSыќџ‡ё§џКїўџбњўџѓ§џhьќџOщќџOщќџЂЦџPыќџCођџQгшџZЬтџIРкџ#Гвџ ЇЩџ ЇЪџБбџКиџ'Фрџ1Ьхџ7Нвџ8ЄВџЂЦј ФџџџџџџџџџџџџџџџЂХјPыќџRыќџ†ё§џКїўџбњўџѓ§џiьќџOщќџOщќџЁЦџЖдџЋЬџ5ОлџJЫфџ^жьџqсєџ…эќџ…эќџrтєџ^жьџJЫфџ3ПлџЊЫџЅУџЂХљ ФџџџџџџџџџџџџџџџЂХјPыќџRыќџ†ё§џЙїўџвњўџžѓ§џiьќџOщќџOщќџЄЦџKЫфџ‰яўџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџŠяџџOЮцџЄЧџЋЫ џџџџџџџџџџџџџџџЂХјPыќџQыќџ…ё§џЙїўџвњўџžѓ§џjьќџOщќџOщќџВбџNЮцџŠ№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџŠ№џџSачџЅЧи€џџџџџџџџџџџџџџџџЂХјPыќџQыќџ„ё§џИїўџгњўџŸѓ§џkьќџOщќџOщќџDоѓџДгџ­Юџ7ПлџMЭцџbиюџvфіџˆюўџˆюўџvфіџbиюџNЭхџ6ОлѕЊЫё ЅЧЮЂЧ)џџџџџџџџџџџџџџџџџџЂХјPыќџPыќџ„ё§џИїўџдњўџŸѓ§џkьќџOщќџOщќџOщќџOщќџ>жэџ(ЖЭџЃМџ ЅЧ§ ЅШоЂЦѕЃЦі ЅЩо ІШЛЂЦЁФgЂФ4џџџџџџџџџџџџџџџџџџџџџџџџЂХјPыќџPыќџƒё§џЗїўџдњўџ ѓ§џlьќџOщќџOщќџOщќџOщќџMфїџBФгџ7ЄБџЂХјџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЂХјPыќџPыќџƒё§џЖїўџењўџЁѓ§џlьќџOщќџOщќџOщќџOщќџMфїџCФдџ7ЄБџЂХјџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЂХјPыќџAнёџRвшџXЪрџEОиџАаџЅШџЅШџЏЯџКиџ'Упџ0Ыфџ6Нбџ7ЄБџЂХјџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЂХљЕдџЌЭџ:ТнџNЭхџaиэџtуѕџ‡э§џ‡э§џtуѕџaиэџNЭхџ8СнџЊЫџЅУџЂХљџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФЄЧџIЪуџ‰яўџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‰яўџIЪуџЄЧџ Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ФЅШдIЪуў‰яўџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‰яўџIЪуўЅШд Фџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ Ф% ЅШЦЉЫђ7РлєNЭхўaиэџtуѕџ‡э§џ‡э§џtуѕџaиэџNЭхў7РлєЉЫђ ЅШЦ Ф%џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ Ф0 Ф_ЃЦˆ ЇЩЗ ІШмЃЦіЃЦі ІШм ЇЩЗЃЦˆ Ф_ Ф0џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџјџ№џ№џ№џ№џ№џ№ќрРРРРРРРРРРРРРРџР?џР?џР?џР?џР?џР?џрџќџџ(  7›Фџ<Јџ<Јџ<Јџ<Јџ<Јџ!„Юџ]тіџ]тіџ]тіџ]тіџ]тіџ#Єзџ!„Юџ@Биџ}ьљџ}ьљџ}ьљџ}ьљџ}ьљџ+Лыџ!„Юџ@Биџыљџ}ьљџ}ьљџьљџыљџCађџ)•жџ7›Фџ;”Ўџ<Јџ<Јџ@Биџыљџыљџыљџ€ьљџыљџAађџ)”еџ!„Юџ]тіџ]тіџ]тіџ]тіџ@Биџыљџыљџыљџ€ьљџыљџAађџ)“еџ5œбџ}ьљџ}ьљџ}ьљџ}ьљџGИоџыљџьљџьљџ€ьљџыљџAађџ(‘дџ?Ідџыљџ}ьљџ}ьљџ„эњџPЙоџыљџьљџьљџьљџыљџAађџ'гџ;ІдџыљџыљџыљџэљџOЙнџыљџьљџьљџьљџыљџAађџ#ŠЮџ;ЅгџыљџыљџыљџэљџOЙнџыљџыљџuщјџuщјџыљџAађџSšКџ9ЃвџыљџьљџьљџьљџKДоџdиюџdиюџdиюџdиюџYвьџDШъџlЂБЊ6Ÿаџыљџьљџьљџьљџ}ьљџLИкџLИкџLИкџLИкџLИкџLИкџ6Ÿаџыљџьљџьљџьљџ}ьљџ7Ицџ#ŠЮџ6Ÿаџ}ьљџ}ьљџuщјџuщјџ}ьљџGОпџSšКџkЁА6ŸаЊdиюџdиюџdиюџdиюџOЭыџOНктlЂБЊLИкЊLИкџLИкџLИкџLИкџLИкЊўќќќР€€€€€€€€€€Рџ(  <Јџ; Мџ;ŸМџ+ŒЖџ0˜аџMнѕџMнѕџTпѕџ Бшџ|Ъџ?Ідџ}ьљџ}ьљџ„эњџCађџ)•жџ<Јџ; Мџ;ŸМџ;ІдџыљџыљџэљџAађџ)”еџ0˜аџMнѕџMнѕџTпѕџ;Ѕгџьљџьљџ‚эњџAађџ)“еџ?Ідџ}ьљџ}ьљџ„эњџ6ŸаџьљџьљџьљџAађџ'гџ;Ідџыљџыљџэљџ6ŸаџьљџьљџьљџAађџ#ŠЮџ;Ѕгџьљџьљџ‚эњџ6ŸаџpшјџpшјџpшјџAађџlЂБџ6Ÿаџьљџьљџьљџ'гџLИкџLИкџLИкџLИкџ6ŸаџьљџьљџьљџAађџ#ŠЮџkЁА6ŸаџpшјџpшјџpшјџAађџlЂБџLИкџLИкџLИкџLИкџјp№0№0€00000p№№‡№tortoisehg-4.5.2/icons/32x32/0000755000175000017500000000000013251112740016413 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/32x32/status/0000755000175000017500000000000013251112740017736 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/32x32/status/hg-removed.png0000644000175000017500000000106313150123225022477 0ustar sborhosborho00000000000000‰PNG  IHDR szzєbKGDџџџ НЇ“ pHYs  šœtIMEп.‹пБРIDATXУэ–=nТ@…Ÿ­єv“†Ц DCCуЦ…хТ)ќфТgHƒхмУBBJ ‰эL 0 ŽН^“ašБ…і}YЯЮ—ИDA„‡џжКo"kпчЕя— ~oЕјЭЖ зщ2‘'ппН0cьyP1ќаj…млЖtЎgf€#з•ŠС‰РDрэVjBW…3 †Ž“)–‡`"аf“k"w ВрItЛGb2x’EgbДМRŽ=/W,ЩЯГ !0"L2xšl?GЎ› б'4сR‰‰Ёуœ ^h 11шvЯW2˜xьt*‡+8|щэvЅpyf…œ‰JIъЪџўkЋД\пВ :;єЊсМџ­WЋ)™аЋ†cџ "м\_уЄiј8'UичР4Ѕ&ДSрг(є-+зxЧ~уЬЮИ:~œЯЙWЋЪŸ†@`@sк„Ўдr}>Д—хђPі4МєA|зlюІr‰C&80ЭLxоHЇсmЃqdBх„  C Ўt­ъu^еыЅ/Ѕ Ур…aTs3ўГkљ%ўE|œ}йзжљнIENDЎB`‚tortoisehg-4.5.2/icons/32x32/actions/0000755000175000017500000000000013251112740020053 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/32x32/actions/hg-branch.png0000644000175000017500000000214013150123225022405 0ustar sborhosborho00000000000000‰PNG  IHDR szzєbKGDџџџ НЇ“ pHYs  šœtIMEп +™Ъ?ЎэIDATXУХ–]lSeЧџЇэњБVЋ ЗБбUHAрШbзЈ$Ц8LќМ2^˜EcТ7F#ђм‰*$т•&„)†t%шЭ$ndc^ –ZГБЎагЎ=mпчётœгvчвsœorв‹Зэяџўпчљ?јŸ—ДвциdТѕе™+пŒ7_міj(иІZ-РЖќшаХhW`C ГЕџШё бБЩ„kMŒM&\ƒ'~ŒFфn’ žмОI>rБL„­іфЯь ЪŒрЛќ(дZ$ТVk;A1c>™ж„#™Ъ‚M[ƒђЧŸEM‹АРбЁ‹б№юGd!t81F~О6:—L—э_H)И|ef”™СаЎЇчсvљєљЩL hoіN\‹c|:މщ?1rijєУо“"§ѓР;OE~П1:3{7тIЬ&‘ЮфާgAєжСяyS ФŒ?ЎЯсЫУ§’е9рXi“ˆAЄеХ>}ўjфЁŽuхњ\HeэыяїŠх'“АЕЛѕX(иІд-€™!ЊDРаЗПьНЯяџ`ъњm3„ bЬЮхєњ!”AU‹8ѕнo/иQWCЏx]РР+OœKЬп*ч‚сUх3УюАУчuoП<Зз-@ƒHTлb/=оїзЭДЖG(зˆщLоmТУbs%’zCcяНБ+2>?љћTќЊ’Щ—ХTЇ'ƒqOЃkЉ~ШЈзDb(и6ђXWѓзіІЧЙ,Ў+Ž ЉЌdЪ"ЭЊQА№lјVІ4мшvњЕяpХЊd‡Яыє˜ЊЁGГб†eИRіИ~QЭ.Љ…дэlЙv@^-q§PЅЭјxCеЩyЕ x\Жз•k3Zš|Њ9—чУўСГсTК8ьv7јU2"—/(Эы<ћN}ђђf ь ѓ‹Y6зњŸyм X4рzр фѓEЅЅЩГягїŸРЗУ}™ЌZ.J_Ѓгa>ˆСэj€Ыщ№m)!Ї–С`з–ЮиЁw#}K9Ѕ’РЮЭ&f3JB+&шme„ŒZ(*­ы—СЕssG €45“є:ьЖB§Г€Д+ЃмзФМэя]сеЋћСІЌЙiЈ'Ё!†Ј…’воќяpKЦqu 03 EЁtДј,ƒЏкf†Z,!Ас^KсЋ.B€ЁЊEX _хhдЮ‚ЕЛвц@ѕ,X3Џ?зѓуFœ;aН€ПЌАDˆђж‚№IENDЎB`‚tortoisehg-4.5.2/icons/32x32/actions/thg-repoconfig.png0000644000175000017500000000154313150123225023475 0ustar sborhosborho00000000000000‰PNG  IHDR szzєbKGDџџџ НЇ“ pHYs  šœtIMEп8ћ#-№IDATXУэ—Ml aЧя;ГЛ3ЛS-К|4 Љј&$(q@Т‰‰‚“KЙ9p‘H8‰ФФgD]}Ё"О **>лmЗjлmggЛяМT‹ЅГkЪХ“L2y&яћџЯџyžџ;џу‡јнУ;чl­Tп/`š1b‰QФ1єfл№z>Ђ 9„Œ ЅЩТ Ў…Р­3†v*'2nђ,'‰•aD‹n TЏЇ7›ІЅљ&]ЏYМY‰В 4žщ uЫ_ЗЌ,Iп5]хЭГ+,о”DBў”ё™єKњМю’СћМ,™t3?№ѓЧ„ж ){ИщUЩ:ьŠ$V"‰эTc;еDb_СКqГir=iмl7лFgЊ‰<#ˆhѕ€šiгё•тS‡GЎВ­ЏШuпФыM!ЅёE(П€e;DэVмBFc˜Щ9ДЅ $щ(Ÿ@иNvхTDd6": ДЦїтeб~+1ЫFˆ2ќ”Їы­ t•T63X РGA,ю€Š‡цђ­тћк…O@ ТзCЂh­‡K1\‡jХ2xSaˆ@o^.-Г4ЧжЌ:В>аŠћ‚q2KяY5•ColьЈёэВнлQS ]l*ХHьY3с/ѕ€Ук€СЧАпrKРЯ+—cwТУвћќму}дŽœ†_‰шт$4šЖьыяrZ.ё.ѓ”х“6‡э?8ћp7ћЏ­Ігm “kчдƒ]l™w!dˆ ќ"Ђ†E&зЮсЦmєљЧяэdiэzЦ˜RО *C+Аvцnоfžђ$uН—WтћŠ†E'ЪŸ) оПxŽяћEHй@l_p”1N -]/и:џ‘_|AR ~cAм>ѓAwІZ[лŽ]й]ЅАтІ,> ‰h;ъOrуеyъЊ„їcвxкавŒЊр‚4ил|™ 3B1Ёu‡ŸќоŠы7)Нƒ2…Р‡LˆN8Мa–+]XёГд craКGIENDЎB`‚tortoisehg-4.5.2/icons/32x32/actions/thg-userconfig.png0000644000175000017500000000172713150123225023512 0ustar sborhosborho00000000000000‰PNG  IHDR szzєbKGDџџџ НЇ“ pHYs  šœtIMEпѕ6Ѕ~dIDATXУх–Kh\UЧчоЩЭ$™Lg’™и:”2D "ОАP|0 нЅКPшJ|!nЂ;•ЬирЪЎК)4‚ ".ЕИГ‚NE|”jЁXБЅЅFHc’ЩМв™Йчх"щ8у$&яшТ\юНчћџЯџ|чџјП‡июgВ kk Žг‡еŠЬЬВш93йЄЕТ2’~˜Ёб4Ў7ФЅЫчЮсЏШ§g$ўvr>—Дсс=Єy™ўшnРbeŒ_Aз‹,^ўŠЅЋgЩЬЌtMТй<ЗB„lЊм4*XЙЪhъNЂЗŒ“ŸNиР €Ы­ї=Eп`|Мв7~ЋъŒЅїc­!pжh"Щл[РЫрnШ#‰“ŸŽл€€ўсБП€WкРo†7УZAрjЫWZdЏ`UmУџъеeDа[ —ЪѕŸš…ЗИ–ud­LfІа‡аІ5` ?Юа№ЁЛi‚љKп€p‚/ТG„Еš_ЯJcЕиёнХТ•oЉцШМЙ$zц„љщQ –h2Mx8ђЈW T–ЎЁќZoА5NчFЌАkЌ(4sХоѕ‚'gћ­pZ›-38Тсуы"0‡Nz6–Œ2Ж'Ю`dEkƒ6†я>9h;ю8“'={л]{IOЄжŽ™в(­бZЁ•рž}ЛЖNь <З9Т-ЯžKШ;ѕK;ЩYЯюŠEHOЄ0кД+­›о8ДЗ7 ИŽCbw )%z№&Ажk#шpк%фЙH_тЏщKЄ”јОяћлNьыГпOэL+@+…яЫцŠ•Rmjl7><Ÿ#ПЇNhQJ#Ѕп\qS ЙІF‡ecљНz­энѓŸё[щ"?Лs+жZ7Ѕo–ОRЊ#С?с­/&YЉ] T_ф§sЏѓмo#Жб#кkРўЙ­РRJŒйјОсЙaJѕEާв4xчьkdв‡IE'КѓЅ5в—h­Pjы=њю#Ь•.raсKВЇbŒfъСwЛя†ЕjƒђЪ ЊЅѕUе1:—Wœ`,Вљђ%žпŒ>зыж еbcЧgyШ‹ёъCя‘Пњw$toDЇ^jlйPžШБa1ЄЂО7ь№пˆџœ@Ј›IЯП?‡јoЯ—wIENDЎB`‚tortoisehg-4.5.2/icons/32x32/actions/hg-add.png0000644000175000017500000000100713150123225021701 0ustar sborhosborho00000000000000‰PNG  IHDR szzєbKGDџџџ НЇ“ pHYs  šœtIMEп) џ№€”IDATXУэU1rТ@ м=xЁ‚:>BI•’ЄЫ'Rаg†I—"?рмi•c vˆS5ђŒ}^нЎДюёпƒM Ц еН_/чЁЩџњM p€Г)BЏ‡dŽhB2Чfёјєм˜~+кH‘ ф@шЄ“C8м>™M­z Uбєм‘фн@Щ:р&$;c  ФLц]KƒпАу…МфQЩРУl ’2yІёс!œ€;‰бdшб„ЄЌ9‹яр}ѕzЁзI;­щ,ВWо|ŸEТ B€Cеnџl“Ѕ9?е</чdbvІYб’Сг№Tг Е мтцЩ„:Њ,`Г ‰ЌЁ˜ƒxСNќу3ё›э а‡Ќ, иjgЫh2teэСп^V?їKћ|0^x4СŽhпыМ^ЮљћV,‡PжМC+NЖ›ѓЃ†SЧыиQюіNp Чc*u.ЏšѓK№Чб†ЏкjїИGлј…@<кА\ђIENDЎB`‚tortoisehg-4.5.2/icons/32x32/actions/hg-sign.png0000644000175000017500000000255513150123225022122 0ustar sborhosborho00000000000000‰PNG  IHDR szzєsBIT|dˆ pHYsvv}е‚ЬtEXtSoftwarewww.inkscape.org›ю<ъIDATX…Х—_LгWЧ?ЗŠŠЅ‚Bˆ˜ЗИИ%fбiдмF•јBPgt™[ЖlFЗ%[цтtXТЫ4Ц%`Ь6E:%’("*д ,QiћнCИŽЕ@|Р“œќrя=ї|Пїог{П5’xžf{Ўш€Нчў§Зя{Н{ жКГмп7[хЖm|>_yп@яŠM›6эwп9тuК\<юы3{їМїAaaсвё$`їћ§qЋV­Z}йусє™3ЖŠŠŠммммЏЧ‹€ ЈMЫШHu:)[Дˆ™YY‹цЭ›—4n<O`ЧіэuiN'z{щКлc3ЦTŒ€ююю}ЎЬLzzzhnЙl>?|xћИ№x<-AИ•–žNŽлMЮєьŒ‚‚‚ќ‘&šА9Œ1/cвŒ1qЯBР н„ ,Ј:Аџ‡W››щxФтW_9њк’%,А8РФGИxєYž`ЙMRзX Dо„_fИнƒ‰&PЖЈ”ЉЉЉЋЧlcL№а#щІЄVI-’š%ЕIК+щ‘Є^Iї€ЙЃЎ:МsЩџ!рёxњŽ?^›ъtвлнMškЊ-???[в5I7$ Œ’дnŒY ДFЫ0Ц”cцcf)@†1ц][DРТŸkkыœSЇrЋНЦІЦФнЛv­У.bŒ™М \‘tлъ›fŒ™mŒ)$|4M’.IjГкk€Ž8cL”tИp­Еѕ§4Ї3wJRН§ю1€g/'УM“јKв(ё‹з%](r€t ЅИИИњ—К:}u№ њ§§ЫЪЪR$1м тыРB`ЄG‹Ет€Р;@вPПMR“uЦї$=ŽdИнЁС'OИЗ+СхrеDYE6Ашќ€зЊ•{1vЩьЎKњB’џщX4AђнБc7ІLœ˜цt’šюђчЭЪ›l%JЪ|рWР#)уt†Рgы€o$u*HъыЪЬЪТїрщЮДЄЊЊЊхVЂJ  8 Љq рs€РЧбРcАйlЛуސУсрnWХEХћПO$§&)4А>Ÿp}}*щaЬРXES]]}щlCƒЮжзЫлщ дддLˆЅрцЫЧSvvvVЧ;ј|>‚С€§тХ‹[G[ЕЕђ@ЊЄcŠI7œ9г{ЉБqrжєщФЧOj-_Yž7 ј4 Kвљˆ>сy8Ѕa€#Њт­••G“’“щИ}›Ysge”””Ф>(tо3ХГЬГX$-УСG%аммМЅ ЈшfПпO0˜\ZZ:’Py ˜aŒйЌž.м;@ЃЄ›б&H@’6oо|4Юс5;чuІ;gБо‘}„Ÿыnр,аcyНЄ?% ЦТUDtvvў˜ИЅЯчЫ\Зz]Jp<А˜|&щЦhЙЂйЈџŒМ^яхЇNМноN ˜k;€€[’v<+8ћюызЌ9}эъеVm2Ч:ї™юсжзжіЦ П'&&ц'%yŸyеіПАФХDўеxvТGeы8tшКнnOь–Є Eё`ФwИ‡kЇoˆ‘4TPI„Џ)@ЂEЮfЙ‰р(Ыƒ@ "yДОˆvДИ ’њяЃо˜Й‹žлIENDЎB`‚tortoisehg-4.5.2/icons/hgB.ico0000644000175000017500000000344613150123225017033 0ustar sborhosborho00000000000000 h&  ˜Ž(  cїІ*\цšZeќЉ!`№ЁWIИ{ыVжяZр–fџЋ_эŸbOФ„ќSЯŠџSЯŠјaђЂL]ш›ХPЧ…џKН~џNТ‚џVжчdњЇ.Xл“шYо•џbѕЄџcїІџRЬ‰лaђЂbѕЄџfџЋџ`№ЁџHЕyџTбŒхfџЋfџЋYо•їYо•џYо•џ`№ЁџPЧ…ћfџЋfџЋ)\цš§fџЋџfџЋџ`№ЁџGВxџZр–БaђЂЂYо•џYо•џYо•џ\цšК]ш›УZр–џ_эŸџeќЉџUдŽџIИ{ўfџЋ@fџЋ bѕЄž]ш›нaђЂЋfџЋ^ыЂYо•џYо•џZр–џcїІџFАvџ]ш›ЃeќЉfџЋbѕЄ3Wй‘яZр–џYо•џYо•џ_эŸџSЯŠџPЧ…пYо•–NТ‚ўQЩ‡ЬfџЋ fџЋ]ш›WVжДSЯŠќXл“џYо•џYо•џYо•џYо•џ_эŸџGВxњWй‘ї`№ЁџaђЂџ_эŸOfџЋKН~ШLП€џQЩ‡џVжџYо•џYо•џYо•џYо•џYо•џ^ыџIИ{ѕ_эŸ­Yо•џ\цšїeќЉ'OФ„ХRЬ‰џZр–џYо•џYо•џYо•џYо•џYо•џYо•џYо•џZр–џQЩ‡еfџЋfџЋVfџЋ*bѕЄ@QЩ‡џYо•џYо•џYо•џYо•џYо•џYо•џYо•џYо•џYо•џZр–џXл“˜`№Ёm[у˜џYо•џYо•џYо•џYо•џYо•џYо•џYо•џYо•џYо•џUдŽўeќЉ;dњЇS\цšџYо•џYо•џYо•џYо•џYо•џYо•џYо•џYо•џYо•џYо•ЙfџЋ _эŸфYо•џYо•џYо•џYо•џYо•џYо•џYо•џYо•џXл“чfџЋfџЋ;^ыюYо•џYо•џYо•џYо•џYо•џYо•џ_эŸлfџЋ*fџЋdњЇŒ^ыж\цšј\цšі_эŸЬeќЉtfџЋ уЌAСЌAСЌA€ЌAС€ЌAС€ЌAŸЌAЌAЌAЌAЌAрЌAрЌAрЌA№ЌAјЌA(  HЕyOJК}ЎHЕyNТ‚ LП€§TбŒђKН~KJК}DYо•џ_эŸџUдŽќWй‘Xл“ц_эŸџVжџOФ„џQЩ‡NRЬ‰™Yо•џZр–џ]ш›џOФ„FTбŒœ`№ЁџfџЋџZр–џNТ‚§PЧ…PЧ…;Zр–ўYо•џYо•ѕ`№ЁIИ{Zр–џ\цšџaђЂџJК}џQЩ‡ЎVж(Yо•c^ыGВx>\цšџYо•џZр–џ]ш›џNТ‚№NТ‚ЅOФ„юKН~?AЃmPЧ…ОPЧ…џ[у˜џYо•џYо•џ`№ЁџHЕy§Zр–§`№ЁџOФ„ИGВxZHЕyњVжџZр–џYо•џYо•џYо•џZр–џOФ„ћVж˜Yо•тWй‘!PЧ…ћZр–џYо•џYо•џYо•џYо•џYо•џYо•џPЧ…рIИ{K[у˜џYо•џYо•џYо•џYо•џYо•џYо•џZр–џOФ„›PЧ…&\цšџYо•џYо•џYо•џYо•џYо•џYо•џVжѓ\цš Wй‘ЪZр–џYо•џYо•џYо•џYо•џZр–јPЧ… `№ЁWй‘kZр–э[у˜ў[у˜ћYо•ЮXл“!Ф0ЌA€ЌA€ЌA€ЌAФЌAЌAЌAЌAРЌAРЌAрЌAр0ЌAtortoisehg-4.5.2/icons/refresh_overlays.ico0000644000175000017500000001373613150123225021720 0ustar sborhosborho00000000000000  ˜6 hЮ  Ј6(  |IwEeqA”l>”h:ee8‰N'†Q`€MРzHdtC.n?,j<df9Рd7_b;‘Y!_ŒU  Ke ‰$eŒ Ї І‰ d7] g9 e8`œa#—^#РLk CЅZЕляр§WУlцЇ зœ с Џ<[ i;Рh9Ёf%ežc$d‹.fЩшањšтІчєќіќGг^ЬЕ Ъœ с‰ dm>dl=eЈj&”Ѕh&,ˆ!Њ4ЖNтР РeъxСфћчђЩ3РЇ з ІsB.qA”Ўo(”Ќm'.‡ Њ  оО Ти Њvь‡ЦŸцЋфІ йŒ ЇzH,wF”Гs)eВr)e‚ g” щ­ вО ТР Р€иф-ЊGщ‡ eMd~KeЗv)Жu*С_y$ … Д” щ  оЁ н˜!ц/žJДJj ˆRРƒNКx+`Иw*ž‹g&.&w ‹‡ Њ‡ Њ„ fUm" “[" ŽW!_Лw3Лy+bМƒFѓ•^'сƒN&Єh&HŸc$Р™_#`b'Гs)Ÿš`#ŸƒN‚tDЇj&0Ѓg&љ№яpйА Pрp@ @ рp PбАчpу№(  wEtCQp@tl=vh;Se9ƒOL™{IэwEЖrBŒn?Šk=Гh:яe8™c7‹U +‡RсƒOŽLi<g:e8рd6+“["X!рŒU ]„ K‹$ЖŒ ф ф‹ Д‡ Kh:]f8сe8˜_#™•\"ˆ&–ЮЃњЬъвљЊ1оЄ кž р“ ъ‰ i;Žg:™ e&b$я™_" L†Ч•њіќї§ќў§ўеѓкєП.ШД ЬЅ к“ ъ‡ Kl=k<эj;Ѕh&SЂf%ГŽ0Йсѓхћ&МAж`йtвјўјќИёСфЦ ЛД Ьž р‹ Дo?Жm>QЉk'vЇj&Šˆ щЈ6тЖ ЪЬ ДQыgЖјџљњuх‡ЯЛ ХЄ к фsCŒqAtЎo(tЌm'Œ‡ щ сЕ ЫЫ Жо ЄgэzНьћюјР4ЪЃ лŒ фxFŠvEvВr)QАq(Ж„ К— чЋ дО ТЫ ЖЬ Дxп‰иžрЊьœ т‰ Ж}JГzHSЕu*Дt)юГr( M‹ ђ сЋ дЕ ЫЖ ЪЎ бˆг—ю' B№„ K…Q‚NяLИv*”Жu* ‚‹ ђ— ч сž р™ хŽ я%–A‚ŠT ‡R™Кx,Йx*рИv*Y*t l„ К‡ щˆ щ† И L“["]X!рŒVМy+,Кx+фЙ};И—_&є‰S?Ёe%œa$Ž˜^#с”["+Мy+Н‚AјЋs9џ‹U єL?Їi&Єh&ь d%™œ`#Зv*€Ѕh&€’Z"€€LutDЈk'џџ№ччм;А     А  м;учёЯёџ( @ uJ wEEvEƒsBСqAсo?эm>їk=сi;Кh:‘e8AmI$}M ~Jp{HуyGџwFџuDџsCџqAџo@џm>џl=џj;џh:џf8сe7|`7 ƒONMъLџ}Jџ{IгxFŒwEMuC qBqDp?"m?Kk=„j<оh:џf9џd7пc7P‡ZˆRŒ…PўƒOџMФKH€€€@h<Ch;Юf9џd8џc7‹f3™f3‹U ЅŠT џˆRћ†Pu€@f3h;lg9љe8џc6Ѕi<X!‹ŽW!џŒU ђŠT>€ ‚ U…  † вˆщˆ щ‡ а†  „ T€ i:Dg:ђe8џc7Œ“["P’Z"џX!љŽW!D Q… ж-єŽ я‘ ь’ ы“ ъ’ ы юŒ ёˆ д„Pj;>h:ћf8ўe8N˜`" –]#п”\"џ“Z"lm$‚ †‹'ѕЭљэїяўEЕ]ьœ сŸ рŸ пžр› у— ц‘ ь‹ ђ† „m$j;uh:џf9ъe5 š`#|™_#џ—]#Ю™f!†Ž-іЛрУћџџџџџџџџѕћіўOТdтЊ еЋ дЉ жІ иЂ н› т” щŒ ё† „€@j<Фh:џf9p’m$b$с›a$џ™_"C R† іВмМњџџџџџџџџџџџџџџџџіќї§XЯlкЖ!ЩЕ ЫБ ЮЌ дЅ кž с” щ‹ ђ„Pl=Hk<џi;уj5Ёe&AŸd$џb$о€€@€ ƒ иcИvїўўўџўџџџмєрјєћѕќџџџџџџџџі§їќ<аUЫС РМ ФЕ ЪЎ вЅ к› т‘ ьˆ д€ €m>гk<џj<EЃg%‘Ёf%џ e%„ V!–=јяјёўўџўџ‹и™ыБ Я<ЪUбьћяљџџџџџџџџуљцєа5ЛЦ ЛО ТЕ ЪЌ дЂ н— цŒ ё„ To?Œm>џl=ƒІi&КЄg&џЃf%KЃ”4єл№пќКцУєЌ дЗ ЪС Р/еIРрњфђџџџџџџџџНѓХфЯ ГЦ ЛМ ФБ ЮІ и› у ю†  q@Mp@џn?СЈk&сІi&џЄh%" е ё-ЊGъ7ИPсЏ аЛ ЦЦ Лб Б!п<Џгћйчџџџџџџџџ†ы–бЫ ЖС РЕ ЫЉ жžр’ ы‡ аrC rBџp@сЊl'їЈk'џЅi%‚ ю №™ хЅ кБ ЯМ ФШ Йд Ўп Ѓэ: сўфъџџџџѕўіњ+жEНТ ОЖ!ЩЋ дŸ п“ ъˆ щwFtDџrBэ­n'эЋl'џЉj(‚ ю№˜ хЄ кА ЯМ ФЧ Кг Џо Єч!œ1юLІь§яѕџџџџЛёФцТ ПЖ ЪЊ еŸ р’ ыˆщxDwEџuDїЏp(с­n(џЌm'  е‹ ђ— чЂ мЎ вЙ ШФ НЮ Дз Ћо Єп Ѓ@фXЗє§іњ§ў§ўYдnеГ ЬЈ зœ с‘ ь† в{I"yGџwEсБq)СЏp(џЎo'NЄˆ є“ ъŸ рЉ жД ЬО ТЧ КЮ Дг Џд Ўб БSмhЩјўљќтїцїБ'вЄ к™ фŽ я…  }JK{IџyGКГr)ƒВr)џАq(Ž~ V„ ј юš фЄ л­ вЖ ЪО ТФ НЧ КШ ЙЦ ЛС Рqи‚мќўќўxб‰шŸ р” щŠ ѓ‚ UL„~Jџ|I‘Еt*EДt)џВr)иЖm$€ € к‰ є“ ъ тЅ й­ вД ЬЙ ШМ ФМ ФЛ ЦЗ ЪБЯкžыщїьќŸ/чŽя… ж€ €@‚Nо€Lџ~LAИz)Жu*тДt)џГs(U} Rƒ љŒ ё• щ тЄ лЉ жЎ вА ЯБ ЯЏ аЌ дЇ и  о—еЄє†Ъ–і‡ ѕ Q…QC„Pџ‚Nс’I$Иw)jЗv*џЕt)вЊ€+!ˆ„ јŒ ё“ ъš фŸ рЂ мЄ кЅ кЃ лЁ оœ т– ч юLЋcјƒ#†€MˆSЮ†Qџ…P|Н{&Йw*тЗv*џЖu)hm$!ˆƒ љ‰ є ю“ ъ— ч˜ х™ х˜ ц• ш‘ ьŒ ё† і!†m$V l‹T џ‰SпŠS Кx,\Йx*џЗv*њЖu*D} R€ к„ јˆ є‹ ђ№ № ёŠ ѓ‡ іƒ и R’X!DX!љV џŒVPМy+‡Кx*џИw*№Зv*1’Z!im_!~ VЄ е‚ ю‚ ю еЃ V€ •]!>“["ђ‘Y!џX!‹Ьf3Мy+ЈКx*џИw*јЗu*uЃh+t–]"џŽW в†R€€@™`#u—^#ћ–\"џ”["Ѕ™f3Й{,Мz+šКx+ўЙw*џЦ‘[јЄl1џ’Z"џ‰SвƒO€€Ёe%Jžc$Фœa$џš`#ў˜^#Œ–ZМy+IС„?хЯŸrџЫšlџ h0џV џ…Pв€LЇi&?Єg&аЂf%џ d%џžc$ъœ`#NИv*џЎo(џЄh&џ›a$џ’Z"џˆS џ€LвzGЈi&;Іi&џЄh&уЂf&p e$ Мz+џГs)џЊl'џ d$џ–]"џV џ„Oџ{IдtDЉk'PЃf)џџџџџ№џџРџџрџќ?ќ?јџџёјѓрЯч€чччЯѓЮsŽqœ9œ9œ9œ9œ9œ9ŽqЮsЯѓччч€чѓрЯёјјЯџќќ?џ№џџѓџџџџџџџџtortoisehg-4.5.2/icons/expander-close.png0000644000175000017500000000023713150123225021251 0ustar sborhosborho00000000000000‰PNG  IHDR ЉЌw&fIDATxкcd 0‚ˆ‰'fЉщxдeцччЯ€)ўŸSхдЉS€ŠЩSœ™•—d џћџЮŸ>mІb}їr S/юьФTгK–,EU„SёКuыШWL|8 \A &Ќ IENDЎB`‚tortoisehg-4.5.2/icons/menulog.ico0000644000175000017500000001373613150123225020004 0ustar sborhosborho00000000000000  Ј6 hо  ˜F( @ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџУlџОhџНgџНgџНgџНgџНgџНgџНgџНgџНgџНgџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšNџЪtџЦoџСkџНgџНgџНgџНgџНgџНgџНgџНgџНgџšNџџџџџџџ—d@фВxрВxрВxрВxрВxрВxрВxрВxр—d@фџџџџџџџџџџџџџџџџџџšNџ&в{џ"ЭwџЩrџХnџРjџНgџНgџНgџНgџНgџНgџНgџšNџџџџџџџВxрьююџьююџьююџьююџьююџьююџьююџьююџВxрџџџџџџџџџџџџџџџџџџšNџ-й‚џ)е~џ%бzџ!ЬuџШqџУmџПiџНgџНgџНgџНgџНgџšNџџџџџџџВxрьююџьююџьююџьююџьююџьююџьююџьююџВxрџџџџџџџџџџџџџџџџџџšNџ4с‰џ0н…џ,иџ(д}џ$Яxџ ЫtџЦpџТlџНgџНgџНgџНgџšNџџџџџџџВxрьююџьююџьююџьююџьююџьююџьююџьююџВxрџџџџџџџџџџџџџџџџџџœQѓ6сŠџ7фŒџ3рˆџ/л„џ+з€џ&в{џ"ЮwџЩsџХoџРjџНgџЙdџ›OѓџџџџџџВxрьююџыээџыээџыээџыээџыээџыээџьююџВxрџџџџџџџџџџџџџџџџџџ  TК$Чtќ>ь”џ:чџ6у‹џ1о‡џ-кƒџ)е~џ%бzџ!ЬvџШrџФmџЎ]ќœPКџџџџџџВxрьююџсфуџсфуџсфуџсфуџсфуџсфуџьююџВxрџџџџџџџџџџџџџџџџџџ™P#Qђ$Чtќ8фџ<ъ’џ8цŽџ4сŠџ0н†џ,йџ(д}џ!ЪuџЕcќœOђ™P#џџџџџџВxрьююџжкйџжкйџжкйџжкйџжкйџжкйџьююџВxрџџџџџџџџџџџџџџџџџџџџџ™P#  TКœQѓšNџšNџšNџšNџšNџšNџœPѓžQК™P#џџџџџџџџџВxрьююџЬбЯџЬбЯџЬбЯџЬбЯџЬбЯџЬбЯџьююџВxрџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ|СBХЩ~УіТїУЬ|УLџџџџџџџџџџџџџџџџџџВxрьююџТШХџТШХџТШХџТШХџЖ­ЂџІ‹tџЖ™ƒџžrS№џџџџџџџџџџџџџџџџџџџџџџџџџџџ€Ф†‚ФѕOЅмџeГхџfДцџPІмџƒХѕ‚Ф“џџџџџџџџџџџџџџџВxрьююџЗОЛџЗОЛџЗОЛџЗОЛџЂ‡pџЯзгџГŸŒў–dBГџџџџџџџџџџџџџџџџџџџџџџџџ|П@ƒФєgЕцџnЙщџnЙщџnЙщџnЙщџiЕчџ‚Хѕ~ТGџџџџџџџџџџџџВxрьююџьююџьююџьююџьююџЖ™ƒџГŸŒў–dBГџџџџџџџџџџџџџџџџџџџџџџџџџџџƒФПdЏоџ€СыџnЙщџnЙщџnЙщџnЙщџnЙщџPІмџФЫџџџџџџџџџџџџ—d@фВxрВxрВxрВxрВxрžrS№–dBГџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџУяШыџ‘ЪюџСыџnЙщџnЙщџnЙщџnЙщџgДцџ~УіџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџУяšЬьџЂвёџ‘ЪюџСыџnЙщџnЙщџnЙщџiЖчџ~ТљtЙ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџƒФПwИсџ­зђџЂвёџ‘ЩюџСыџnЙщџnЙщџPІмџФШџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ|П@„ФєЂбяџ­зђџЂвёџЩюџСыџiЕчџƒХѕ~РQџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџФ†„ХѕzЙтџžЯэџ•ЪэџfАпџ„Хі€У•џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ|СB„ХЩ€Ті€Тј‚Фв~РIџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ№?џ№?џ№0№0№0№0№0№0јpќ№џ‡№ў№ў№ќ№ќџџќџџќџџўџџўџџџ‡џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ(  lllџ>џ>џ>џџџџ|Tџ›iџ|Tџ|Tџ]?џ>>џџџџџџоооџоооџоооџоооџЄ  џ|TџК~џК~џ›iџ›iџ›iџ>*џџџџџ___џ___џ___џ___џЄ  џEEEџHџ…џHџ…џК~џК~џК~џ]?џџџџџоооџоооџоооџоооџЄ  џ’’’џК~џ$џmџјјјџоооџК~џ]?џџџџџ___џ___џ___џ___џЄ  џ………џ€€€џlllџџџџџоооџоооџоооџоооџЄ  џ___џ’’’џlџџlЮџџџџџџ___џ___џоооџоооџЄ  џlllџlџџlЮџџкџџ~КџџџџџџџџџџџџџџџџџџџџџРРРџ€€€џкџџкџџДџџџlџџ’’’џкџџДџџџџџџ’’’џlЮџџlџџџџРРРРРёсррџсџёџџџџџџџџџ(  lllџ>џ>џ3џџ џ—|Tџ˜gџ|TџtNџR>џ++џˆˆˆџыыыџоооџоооџоооџЈЅЅџ|TџК~џДzџ›iџ›iџtNџž”џЁЁЁџ___џ___џ___џžššџEEEџHџ…џ<ѓƒџК~џК~џ“cџЎŸџыыыџоооџоооџоооџЈЅЅџ’’’џП|џG§„џёёёџ”вОџ“cџЎŸџЁЁЁџ___џ___џ___џžššџ………„„„џ{{{џm‘џtЉ­?ъђёŸыыыџоооџоооџоооџЈЅЅџcccџ‹“ЄџlЉџџuвџџ:иџэѓѓ—ЁЁЁџ___џОООџоооџЈЅЅџlpxџlЅџџuбџџ`Лшџ(‘ХзџџџџџџџџџџџџџџџџџџџХХХџ‡Šџкџџ™уџџœоџџo іЎ‘˜›џ–рџџЋџџџџџЊ’’’‹œЄџlСџџlџЊ№Ф€‚ƒ№‡№Ч№џ№tortoisehg-4.5.2/icons/22x22/0000755000175000017500000000000013251112740016411 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/22x22/actions/0000755000175000017500000000000013251112740020051 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/22x22/actions/window-close.png0000644000175000017500000000200713150123225023166 0ustar sborhosborho00000000000000‰PNG  IHDRФДl;sRGBЎЮщbKGDџџџ НЇ“ pHYs  šœtIMEз PЖИК‡IDAT8Ы•ЯoUЧ?gfоЏООібhлаа’H…šƒ К!jŒnŒ‰ Qv*Kљ+\Й1К0šИ1КjЂ†ЦЄВ ,РRkБЈ­а"§ёx?цН73oцЗ?^IЋ–“œЬмЙŸ{ц~ПgF.Ž"‡:Щ:ЩІrИтАЇP­&IЭ'˜ѓiО4z‰№˜›я|зэлї†“1Х@@ЄmЕjЏjЖвдœ4ѓnмј"Љ‡+ Ч@~hрlсѕЗ%“7 n q\TŒ“@B+DуЂ„ 4ЊЃЁOКюL&Џœ *ЫуРЊHЭЁ˜Ё2Јїg,P\舂k&†Єe3Ž Ž8DутЌ жŠ€x€4 ЂhуфŠx}‡!зeуяYДzпBM Ц …^шС_ЃugŠЄМ„8JУрm€1 ЈкJд@џ2ќИzыL} ­$ ЄГpфyфЩРДа_'рю дФЈЎГg›Дq•{pћ2”Ёgž!Hb{ЦћиME`ёєъчшќшƒЛv§zxлTO"а]šй $з …^фш‹hГ q}rEX›ЧL~ŠN‹†>Њн lа8DХ…Ј‰^џ вyфиЋа?‚sъ-ћVнћ!ЈЁ7/ ?CГВЃЗЗW5аРG§Uh–бв_8ћ"ƒЧЁgpыбЅiЬЅ ОЖkгl‚Е`Vў@Ќ•д€ПjЧGAНєЏнИ%^ •{а ,4•ENŸGњGь}шлTƒє ЇЯC*ћпo 7…<ѕ rќ5ш(Тт4цђ'Ж’SяРРЈЛ3…ојЦкtwАЌ;фРгШи9ЄїUЬХїаŸЧmњЋ8o~lчЦЮY[.ќиўIzш(МЌЕз‰3Ш№IБъЯNlЖВоš@o^dј$rт zэZЧйЁbЧC2y4[ Ў,С•ЯьўзОDл…ђKШї"ўК#*KhЖ€˜$m`œ’Ю“,п&љю§нх61ќyеfЛ z†Р @У-p­IЋІJХ|“ЮёH‘ъІVKЕ&­ АЮU)Я.6'ЇЦК:ДˆьЊPm”ЫП-6'чЊ”@žШђјБЯіvp$“ІЈэЂў0aDyЙСЬѕз~XйЈ- tныу=ўѕ0@T€*Щі7ѓQBл’П'ЙЮФэ”2IENDЎB`‚tortoisehg-4.5.2/icons/thg_logo.ico0000644000175000017500000014336413150123225020141 0ustar sborhosborho00000000000000 ш–(~ ЈІhN €€ ^7Ж@@ (BJ00 Ј%<Œ  ЈфБ hŒТ( @ ..'5 VLB;T:*a=l9ph]‹†ve e4БrЉІКЦЩМъючџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџVMsџџџџџџџѕKƒџєЊc‡#џџџџџgs№ЊЊQkџџџџџ Іb№%fbe!џџџџџ Іbˆ‡3xƒ џџџџџђЊQˆw8Л‡{џџџџџџxƒˆs4џџџџџђНƒ8‡s3…™@џџџ№jcЛЛˆw333И™•џ*QѓэЛˆws38tЩ™•@џџџОлЗss37;|щU@џџџлИˆwwxз0>™™џџџёэлЛx‡?џ§Ф„џџџџ эЛЛЛуџџїШˆџџџџџююиџџ№Н@џџџџџџџџџџџЧџџџџџџџџџџџџџ№1џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџќџё№џррџР€џР€џРџрџ№џр?€|ќўџџР€џ№џСџџџсџџџџџџџџџџџџџџџџџџџџџџџџџџџџ(  ))" : 4S3DO@VLD^?sn_1‰ZTˆT‡…y›^ЇЅ ŠЖŠПТКџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџђUџЗp@џџјƒ№‹"…џџіВzwЊџџ№wzQQIџhNЪuWM“џўъwWЇщ“џџЮЪЬ­tџџїЬЏџњtџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџŒˆ€€р№јxџ§џџџџџџ( @         "#$$&-"%#!$"&&-(,(+)%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`xMgihAHvKIxFzlhzncqtY…G‚og~rgorpuvcux]ˆH{tlsoƒvk~yc~voyzg"ˆV„vrz}byr†znx{y~f‹~s†ƒd†~wVU‚o“rˆ„mŠ‚{[’Zˆ‹p”…‰ŠwЃRІˆ_—^-›d‡†’Š‚6›jЄŒyu”‘r”…—”u‘–‡Њ`Ѕ’ƒbЂd›˜yfІhœ™”Ÿ„ДkЃ›“ЄЁmЎoŸ˜Ѕœ›Ђ ›ЉІ†vАvЃŸЂЋЃ›1КxBК}РЅš+СpД=НzЏЌЊЇЂ}З}ЅЈІNН†0Ф|HР‚ЕДŒЄЏЈЗД”ВАЋКИ’Н•ŽТЊЗЉЗЕА_Ч•ЎИВEЮŒЛЖЙМЙДСХЁТОРТРЛЌЧАЈЬЉПУСЄбЁЩЦСгЩШiуЅаЮЩћиГбдвйжбпмзурлЮъврфсиюгчъшїѓѕђѕѓљїђџќїџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ џџџџџB„@ђ{C џџџџџџџџџџџџџџџџFqЛГeџџџJ№б™jЅ• %a џџџџџџџџџџ ЬY}Tџ,тЭКЌ<3\†Ю )џџџџџџџџџџ фЬu†џ@gw\E&_I02џџџџџџџџџџиЬŒR+![ІЋЉrLLxІДV 8Џџџџџџџџџџџџ,и™F5˜ЗvnrЕШШІlnУ џџџџџџџџџџџџџ:€І‘SАœ}2"X'€V9 џџџџџџџџџџџллІPXАБš‰ZA;46'*ЇhЎ‡.џџџџџџџuзyXеХЪТЖš‰k`AO7;SН]ЩЩОhџџџ(ВџGќшмЪА—”jeZQ=H­v^ыддЎˆˆ.џџџџџџџхішгТ—‰e‚e`H`rУ…рљоˆ‡‡K џџџџџџњѓхЪЇЖЂ‰}||}šшl^fљаЈЎОЃџџџџџџџієцЙХАš šАяcџџџџеэdЁ?џџџџџџџџџЪќяхгЧЦЦщѕ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{{flb‚}h.]ƒk?f‰€z~€~YZŽƒŒŠ{‰‹y_—_‘ˆƒ”Œ‡:Ёn/ЌnЁ—‘ŒІ„ЇЁžЇЂŸІЅЅЊІЃЈЇЇААЏŒХИИЗОКИАЧАЫЧФЪЩЩ I:-7 1$J+96;8=.0%B "3XN?* (SETUG4&!)C,V'ARWMFKP@L5#OQHD2 џџџџџџџџŒˆ€€р№јxџ§џџџџџџ‰PNG  IHDR€€У>aЫ IDATxœэwД]UЕџ?sэ}к=ЗпфІїB’R(!AЊЈˆ@Qє!*њP|ъГМЇ *–WD А€ " RHЛЙInoЇьКжяНЯЙ Ђтћѓc}Ы>g—9з,п9зкPATPATPATPATPATPATPATPATPATPATPATPATPATPATPСџˆ1цО† ^DЄИ8G”:йh§p7p0 x+0xИЧѓм‹~‘1ІВ§л€ZQВ0U55см“›Њšš0"вРTзе…ЅŸЯОиwU,Рџ!ˆˆT‰Ш}Nњ—Я~†г.\†eY„aШПКƒŸ\їuъ›šИњц›1~,-;wђ“Џ}нlYГFˆ”ры‡ЇzcnЅ‚— IЫxy\Dz1'НїгŸтЬw^ŒeYX–Х™я̘ї~њSєvvђд`ЬфЩ|њ{џ#г;Ю_‘)‡oEўЩ!"bYжХЪWDdQ:VуЇХYяКфE?ы]—0~к4ўДт—„A@1—'•Nѓх_D№ЩУџџFDdЄˆМUDО "ЗŠШ="ђ˜ˆЌЗ,kЏeY-"ВVDў "?‘o‰Ш"2SDфОўƒDјO­ѕŠQЃFe?ѓŸ‘bБШй—^њWЧn}њщR|Рй—О›žŽVоw_љрй№Ф“Œ7Žy'ŸŒˆ\&" хОбЭ?M>ќA)еЮ`Аc”RКЎЎ.;vl8gЮГdЩГtщR3}њtндд*ЅЬ Žяю>ѓFп[|Ђ”:[DєI'Єu2^|ЁIg2цчOЏ1+6Ќ+oџўпп5Ђ”yћхя3+6Ќ3?zIe2fмQS `››ЭOV?iЎќъWJїМЄtћ5Rоз"2X&"ѓъъъє‚ дмЙs)m“&M‰\) тAЅЕІЛЛ‹-[ЖАjе*ќёк'Ÿ|ђЌ\.їfрЫJЉЇŒ1пn7Цп›M2‘€_477›[WмЊ25vэкEѓ˜1и‰Ф‡ўєКo`Œсw?О™Г.Й„њЁC6f э­шnogпŽ 7Жє‘ Dщсџ ‘EРoЄЙЙYŸўљœwоy,^МX)Ѕ0Дж ~D#IkŒсp@Djjы8ў„8ёЄ“јЬg?‹ЕЌ_Пž+~СOњгуz{{oQJ§—ˆќј†1ІѕuМ_‘kjWмОB†6P№ ьйГ—ё3gql†t:JArpя^ъ‡Ёyє(Zwэ*зu№3.,§:БєУ?ЕˆШ"ђŸР’D"aо§юwЫe—]ЦЂE‹ZkB­ё|­ : УPkty3h­ЁЄJPЂА, ЫŠіJ),Ѕ˜5kЧЬљ:_КіZѕЛпўŽ›nКБцёЧПJ)ѕ~Йј/cL№šпxŠБx\xю;Ю•Ч/ рш+ібебЩq#Fqhwwtv$Ъюіv†ŒA ^Њы8(Ћђ•cžJ‘Y"r#А(Nы+ЎИ‚ќу2bФТPуaЈ ƒ€ №ƒпї ‚H‚вџuэУЃ#X”eaлЖmc[‰„M2‘ ‘L’Lкœўљ\xб…ђєкЇЙђЪЭЌ[Зюњ8€њ1fхkxя"J>fŒБЎќш•xЁGб/R ‹dkЋщяю9тјКІІˆˆ|Шˆсєwї`йvY š†уро}Ѕ•MУ?•ˆHИZD>eлЖњф'?ЩЧ?ўqU_п@†8ЎKрGТіƒЯѓё}ЯѓЃЭї ƒ?№ CM[ЃMДgа(ўHј ‰щtŠt*I&“!N’JЅ˜={6+W­’›nК‰/|ўѓгsЙмc"r pЅ1ЦyеТrDЎ•wЯž3л,<~ЁфМП@ЮЭб<Њ™Ж––#WJ1lЬюл"Œ?€Ж–2й,ЙО>D)†ЧіuЯ–>ЖЛєУ?ˆШ)JЉk­',^̘nИ)SІ†Ч)тћЎчсКЎытћ~єГчE сЧ DJj]Ж:tf№|eгЏ,‹„mG N“­ЪPUк2вщя{пхМумwЈO|ђќњЎ_ПO„™"rŽ1Іэе|іWьЙ†О§мЗу‡>nрRє‹фМУxющэѕ™\s5_§р‡ИшЊRлexm--Lš9“эызqњ…2dФpnНў!DФ3Цl,ŸяеМјW љЄˆ|ЃЎЎŽыЏПžїМч=AˆыyxЎ‡уК8Ž‹ы:8އу8x~€чЙј~ф‚в>Ѓи TjД‰•рpQˆHd 6Љd’L:EUUйl†ъъjjЊЋЈЊЪЭfљЩO~ЪМyЧђ…Я~ОRВFDоrј§Ÿƒˆ’3N;у4|)@С/P№ Œ›1ŽЕ­eгъе‡tL›7ЏоО‚б“&Аiѕjr}}Н`>g_њnfЬ?ŽŽxъС1цVcLWщГoЈˆHИИtўќљцзПўЕ477—ЭЙS,RtŠХ"ХЂƒуКфr9:К{#гmл~@†‘џз‘Аƒ0ŠJA`jŒ‰•!ОжQJ\Š w ‘;ˆ\AuЖŠšъ,55едежPSхУўWІLž,—]vй(Ч)>!"ЫŒ1ќGŸ‡1Ѕд™MMMzіœй*чч(Х(№‹Ьы|юљб=ќёЖG(P>Рo[A*“сMчŸGЖЖпuљбЏХh №­У?ї†)@Ьм§ж3џНя}/7мpƒиЖчћИŽKЁPЄP,Фћ"Žутy}ЙЋж>ЧфqЭŒкD„a >,ј(щэяЅ*S…ж‚ж%сGŠa Ю(ЧХ%EАs‰dŽL:MUUšккryыы№<Ÿ%KOсСR,;Пъ@kыН"rЎ1цžрyH:m0'ŸѕцГ$0^шсјEПˆИ$В ц5еw?ЪЮ™,ђзZуљЌ\УБsŽ&“ЮЦ@Іёў0—щ‚ФnA•нBUU†кšjъijЊgHcѕuЕфѓМщMЇшƒИZыХ֘Ї_pЏb^ТCD”Ѕ>ЃЕўкнїнЭ‚Х ш.vг>аN{ЎЎbЮ‡:јс•?$№4ŸПщF&ܘQўŽн[Жpэћ?€[(ЂЁaшzк;АSvЯ‚Ы,JOюzфšGТв5Ню "C•R+EdЪwм!чœsNYјNб!_(ЯхЩ ŠE\з‹Ѓ§€ №ШXЙf ѓчL!iйхQ–іe7тz+ŸXУIЧGЈFД)Y‡СX ВБ;0ƒц@$ŠВmЫ"‘LP•ЩP[[MSC=C‡62lhM 8АŸгN;UчsЙ­ѕB`ппКџПЅ _ќте—ОєЅMЭЭЭGmy~‹№шЪwб–kЃ3пIoБ—МŸЧqZvДАтs+№cN<isчђмГЯВ~еи)‹љЫŽЃcw]-]4mЂа[`яг{ЉЊЏк1gйœГ’Ѓ’ћJJ№ККЉWJ=L§йЯ~Ц9чœC ?—Я“Ыч)ф ŠЎутХ)_”о]­C ђ/ЂaЈё\ЅЂ,/@›8Œ…ЎСМР ”РCЉFЄ”Тђ|\зг8F1к J1vь8~qл/дЙчžл,bю5&q‰RС‡wЈиЅс;h§Cљ+%DфZЙТh3§вї^J`мРХ œРС}ЅПЙBŽtcšГ?}6Яо§,W?Ск‡&•M3юи1Ь;gЕУj™И`"žус9Оы“H'иЙjч”uwЎћ§Т7/<‹_б „Џ›ˆHFDў`Œ™}у7rёХAьEТЯчѓфsy EЯѕЪТН(Тw\%‘Џќ№H  u9 Bу9и–†f0H<œ!,)@<њЕжФ*гЧ%о@СТшšтРS)E2™$JrТ‰'ёНО'ўа‡ŽV*XcŒIN=ц†ŽХжЕk&uwtм J зZёp%Dе‰iЁ П=qвDѓёO\J~Пр№_GщmЎŸ|>яњdГ,zз"Œ6 t а4Ж‰T:…XR.sнŸжšщgL' BvЏо=}ѕ}Ћя>ЉюЄSd™єОžрЛ֘Eп§юwyпћоG†QnћќBО@>_ шИƒТї§xT‡qАт:–% M§‡сayHшВ з%™JDAD„жх=‰„Тqќ#вУщШ” Б ­#ыNЅЈЉЉІЁОЗЦуДSO#[]ыКЩѓ›Ь9сDпдuW}дl]ЛіъD"ё;пїзЫ5bЎ‘kH|#1MЛњn ѕЃŸќHTRс8qрКјкЧѓПш8Щј1;(јPkТ Фq}вI /~P>NіsHБшP•NсИAЦqDˆe žлGлчйИ~5]{AQbHк‚­<vфb"š9 тkі#7P(К Nœžz єpюЙчR,љреWsє‚ј~ф*D Ї/[&"BW/]ОTцн3/}э—Џ§за WлЖ=ёЧ?§БЬš7+љ~Ђ_Œˆ ЯЅЛЏ›оў^Š…bcгюљ~@шGƒDЧЪЙВ(˜ЕФB,‰,ƒР”ЅSА…ЎТхKЗ,ЭМц@DЦ)Ѕ~Чuƒ=О@рaDк„ƒž Х(,љх0Œ}ФŠ„Šd"ŒЪЂш…=н=Œ1ЧWјWЮJжEgв$’ вй,љ‚пQ%‚иHк вщ&єљХm?ЇyфHцœt"že0Уг?Ю!CјЦwПAЊ*E"iЃEвUш"аQКчщШм{Ё‡xtїwГmя6кКꀘظiпеЮГП)WѓНKGѕ m4 tфЊ”ТИ†Ож>њFлЁ]‡Ь@Я€$ГЩяž§§ГoћеВ_iлГ˜їЖї/фбпlœжпUјžˆ|WD&šir1цoWi­G~љЫ_&‘H š~ЧЅшq]зѕ№§ жвh3ёˆ,:ЅНжQЇStPb *Kœ ”||Д7qŠщ!Ъ& ’I›ЖЖƒ >’М‹Ѓѓ”iac№}M"ac[ N ?vJŒv‚dB‘Э$ШЄ<јч?аллЫ;/Н”РѕpпѓiнЕ‹žі6.Ишœ0њ›4Ё №ДхѕЁЦi^ЅzЙbŽНївбз[пїЉQЧдЅSIeSьxt-ЯЕpр›{дXRеЉˆяАe)Š}E:їwвлй{ј€ ”ЅžKf“џѓі[оўу_-ћUQ-`№Сњс)~Жў“ъ/xŽUПпšXћРігŠyя р•RыŒ1П~iŒйћFƒRъs3gЮ4—\r‰”?/ЊьEпaбО.EБс (UђJь]Јq̘ВžЇу|?,wсaСw! 5фѓy‰$ХbD)!бо‚0j!eХў?j›%(BЖА’dв 2щ$щЄХЊ•+ЉЉo`Ю 'тЙ.žу CЭЦ'ŸD–œЙ„~ЏŒ! Р“h”—” аЁ‰zŠn‘ŽžzsН‘Яѕр> П`<ЦЧ5Вх[шмнЩюЭЛ_TЪR‡RUЉ?YIы;aЏМpђКsю9ЧYЮr#sБќB„Яоў­UG˜ЋдјЃ˜љІSјDђ\Еeu ЋџДUїlžнuh`pRъ/֘Р֘ƒC>ЂЕЎНюКыˆSŸˆчwнЈ”ыyƒЅл№0AЧЃПdš#PњˆыИиJ‰›Ѕ.›8qЂ>ѓЬ3UчбЅК~Б+€Jкш˜Š=œЅ;rє—”ЃшК(; rB­ЫЎТ”\F)ѕ 5žS “JF   A IЅ“Иn‘PgЫТЌM”Ъщ EЪF%`@€R!–ВI%ЉdЫ2lкДЃ5“gЯ&№<Я# CЖЏп@рћъ"ђ~зИ1ЃчсŸ@ћhtYј~:8‚…,+‚DћиК–џŸШ$6cУf ‹"џ<~ѓуbЯ™Ÿ=ѓзз,Н&ИCю`ЙYn /`Иb €1ц!9ЫаwўцsћЋчНя šrR-uедeЉ­nЄnl#§чqМџкгец'[xь7›дЪ{6Ÿ\p—ŠШ "ђp;p˜pщЅ—R2§ЎчFЙ~Ё7uИe‚ЅdђMY€‡бГeџuђфђDў;Žpы§Б?їŽы­ЎСѕДжєчŠŒ9–чwэdтдЙДwхЪJjУ№!Yv=ПAcХ”ŸD’–ДJ Z‡lXПЫ˜0}zШhУ–ЕkЩdгL;vљА€Ѓ‹јтсууkƒ) _—њŒЦ„1п›б4xіейjВѕYвЕiе‰ђР#И.m;ки№ру{>ЩЊфеw|тЧ|МЄIУNsX?€1цЯ"2ŸпЏН1œвзвЧДsrДі‘IeT†ЩМћ‹ љрзЮPнЧcПлdџхЯсфНГ|AИф’KŽ(єф њsфsљrAх…E™#,Тaн;Z,KШхrd2ЩСбn"хˆŠ<ƒ–"™АщъъЂКІ&VВшяvЖпѕшnЁЙi=J)ъГэmЛhllРї<ŠNŠAат! eХнзOрљlмИ‘Qу'ЉЊŠJАZгyшэ­­œtк‰h+ЄqХС]2ћ&,B•К—‚ˆк ќh ƒш˜н+wГщСMG ЭЖMУ<ЯЃПЇП\бЕlkKЊ&u…ля>ёRJаG(@ЌлEd№Ыаgvl6циXТАќЭГдкћŸчлљmтјуO`ьИqxž_ютщээЅЗЗ—|>Oбq№§ˆžЈШЌ8L˜%з=ЈР9zЦtќ0ФuMЅжІЌсЅc-jЊГ4дзQЂП…ЁІГЧcцœљьxn=---47Ѓш8tэЯ3aтьTЁў N}Џh4 LL€}|ЯЅЅe}}§Ь9љфX`: йЕe Цf8‹?G1(р) 2хБu+бЖZыВР}ЯЧw}<з#єCк6ЗБщСM$3ЩgTJ}Ю/ју0LдFOьlя/"EeЋ=Лйм<МЌЖѕПЂ‚1Н"r6№™оѓЅGЎ дЌ‹,5nqФd:nЧ-ажЕŸt2CMЖ‘”ЉХщLбБнC‡K)~tгMŒ;–ЁЭЭXЪb ПŸўx‹тП\rM&SX‰JйX–Гє`}Офќ`АѕЋЌ˜Ве(Ј\.Єiјћђqћ„ЁІЕ­РШёГIZЎS Іо•bgЦLѕ Mъ+Ђб(`Д‹Ц­аA€ч:8‰Яmл†RТФGЃƒШЭьлБƒlm–nнMw[7vV‘H%;Ђ”~ЗБ‡~XІy}з'№zїєђдO‘H%Ž?nќyЧ|є˜§w,ЛЃєT^5Мh-РЃЏ‰ШŸЕЯ/ж§<œвЖЩ0ч=Щ,хзгQdЭ­-ь{мQ…(ХЪ•+YЙrАuОЊКšу.dцЌc6|Х|зїЫ3w’Љ$‰DŠT&C2‘DY6˜RY–ь_ИёW›6ЯqЫА6Щ”L(‚PгобЇњІ@c]–žюš†E ›eE=ФјшаЦї]ŠNK)vюмIІЊŠуЦЂcіВ˜ЯгqрУŽjІЗиCЪN‘R` ЪЈX™cR+&БB?,ЗК‡~єћž•{Xwџ:”RC'}ћ%_КЄuљвхšПыЭ_E8LжŠШ\р;ŸешйЅѕБ—лjШQBБз№Ш5пщЧЮcс›оDУаЁд65Q][K_Wэ­­Д8Р–Еkyј‘GxшЁ‡ШVз0џИу˜2e*:ІY“Љ‰DŠŒя“JgHЅЋА-ЋLаФзBЉSЇ\I/џ}P9€#В ­ЃNЂЦк Йv†54ђќОјjАm›†ЌЯі–CGІQj[ š є Е‹я+<зІ @ыжжVІЬ>)7™ьпЕ c4ѕЃъуў„0аˆыcФ”љyFf_КЬг‡AˆзчБўюѕьn?‰tb}УИ†w^qУ;x-Ї1ЧёѕQ€јСцŠШ}n?7Џњvа0љ EЁC№јЗЏyKN&Œ[Ж?ђ…™Њ*†Рi\@_g'ыV­тЩћячбGхЉ5O1}њt&MšJ*"JжЏЩT ЅGдчџ*E2њˆПSЧaф“=7Ъ6<зeР №]ж§›˜8i2=Н\?ЄЖ&M:ЉиВy#GM›Юо= KYиЦ˜€0№ё\Ёh+ŒбєtїЂЕfТДi1•]гоэлjGжFТїЃsaTC*@IшFќŸяdл_ЖЁCMК&§НЗ]њЖЯВ”тђЅЫKТMzї^r9иѓ[Y ќtч§њtaютХ{ЪRзvœ —R"FЅА ~:'œyž|‚?н~;Я<ѓ ;wюdс‚EŒ5хии–eйЈxюоaч?ќj(ЕtыУ„„сф{^<‰$К&ЯїШєбP_ЭШ‘УиБ§ЙђtА]‡H&L:•ЖЮм‚C2!Ё2$- _4^рс8!ŸЗЋГ ХЄ™G—ч хљчЉ’E,‰§zЌ*3h™Œ6(}{њhниЪŽ5;L’H'ЋiЎЙцЂk.Z9ђƒ#УkИ&Š‚^й/Г-мsPDЮRJ}јlw{;н=$SI?Р+:ИN1hМЈ‚Ї•РВmьd‚ЃцЮeЦќЌyјaюЛэ6x№Я}є,Ž?ўј2љ!D ЦШсPКŽ#FМјј^д9уЙЎsш^Tkb…lkыРВЭУFR0+hIDATNк:ЄЁqОчБuы.Œ–•Ы`I‚d"š:>aрƒбЄв)Z[[i9‚њ†F‚8–ikiС-iš2ЂlтcA”`+бBџО~кvДБ{нn3а3 "Ђэ”§p§ањ/_xЭ…Ћzzє—§P ТЋ№Н^ICˆ9џќѓ?чw.оЗcЧ‰;чmŒŸ6)Гf1nђd‡ ‹X,ќˆo”@)…Вm‰Щtš9'žРдйГјэЭЗАe§:іЗЖpійoeвфЃP–`)…6@Xт3J)]™јРfy^Йp>Ё ЬшаqЃ@!Cь(sФŠЄTTхГTˆ%>JЛ$Ќ щT% зuА-EgG'Хb‘%'/‘rєпВs'Цъ†зaŒŽSBEю`ŽюНнtьщрРЎ„A=ј„Н-]“^1tиаŸ]~гхћYŠ.бЕ”mЪkWмдмм|fggїt^МwлsьлОЫВHe2Œ;–‘уЧ3nъ2UYС„#q›’ma $гiR™ —\u›žzŠпџќчќrХmЬ_АЗОх$“Љ8ЗиПЈЩ1Рш №аCŒŽ˜DС ˜x†­љ|Ѕ0Ж5G”b›H$H%„„щtŠd2чЛ$ žпЕ‹ІсУ™wвIeііюиNЊ*I*‘Є{k7ћ:9АћЁФм„…ВеcЉъд]йЊьWоvхіЭ›Эб[Ž6Ы—FšПEзО–xйѓ$ъ‘JЄгщ‘a.5FGDеšвHКЦІч€Cq ъЎЕ ЦM™ТєyЧR]_бИ&š€Ё, ЫЖIЅг$3ЊВUxЎЧ}ЗоЪІЕkIgвМумѓ9сЄЅQ С)R,x8N1fcяј~дМс{^м;Е‹wwЕгй~пїAт4UЂљ€vТ&[]GMm-U™, &Šц-%бдqЫТѓŠ:x€\.G>ŸЇНЃƒw^uSgЮŠKПНЌјŸџЇP$ ьd’ьАQЄЋы8Иљi~ѕ…ћП№ЎВа—GЃ§ѕ0ѓWžЏPЊ,Ы:еѓ#2cёH5Ез №нЗ0ащгНпХЫeлŒ?iѓцRзаP(…mл$RЮfЩTUБїnўpыЯ9АЏ…бЃЧpсE—0nт$ љBžуOо?ЯЎZ‰ЉІfјhЊGŽЇzш@иєћ_яj вЉдФbЁИ?:х+єУёŠf‰Ш4QВЪЖTУлџmЎŒž^Ы@_‘|_—ќ€‹WŒ”!звГ?Фэl;СŒуŽcтєщ ŸВm’ЉЉtšTЌЩtŠgVЎфС;я"?аЯиБу?acЦŒaдЈБд74бŸ ЗЇ‡эл6ГiУzДюGЉ“ІЯ`Ъœc˜6guMхІNˆъћ§=НlйЧЁ}ћ8Иw/mћїгокe.ЃFŽbдшQ Z[[щщэeкœЙœїС”Л|Я#№}~v§З0v’q'žŽи P6їБїЉG)єv!Bh s€ЭЏiHџ №J,@ZDV+Kf}єЇЪИйѕхЮ^ЇPЄwшsш.’ыvЩѕњ{4љMяѓ у[Œš8™ ’NЇЫгВ”Rбъq\ЮdШTW>+яН—э6аЖT%У№ТыЎЎ­хш ˜Й`“Ž>šd2ЯѕŒЃ’zмB<йЃ<+TЂЬ"п?РЖuЯђмКѕькД‰|n€T:ЭМ%KXњЖsHІRQащ8h­йБq#нuЭгч‘ЈЉ#пнAчžэф;л0@ЊЪТ/†Fkѓ$бъ\Џ§3/ЏD~\ёЎЯЯёLРѓ]\ЯС \7кŠХ"Й~Яs)єЛєєщk ёњ-КŸЗ,ъ‡ сЄ7П™d*E„ЃQq€˜HЇIІRЄЋЊHe2ЄЊЊHЅг„AРЁ§ћйПk­­TеTSSWЧа‘#™0cFдГ'wРFuSZ3( #*Ж”–J”žIм?ЇDP–UvaагбA*›%‘LF­зею]—0ž”ёЛ[nсрО}бR5ž‡T"e+УвЫЧвБнcуУ> ќ№ŸЩ М,‘йРКcOŸ јіb<пСё—\ Д*м>EзV…зoQU]mц/Y"#ЦO №\‚0Œђџ˜ЄIЄгQ|Ч‰t ;‘РЖX–…(‰Щ jBЃ1A€„QЩжѕЪ)c9bяЗl ,kpmлŽВx жˆnжх5‰|з%0†оЎ.юКёFА,RЕMЄšHз7бімzњZї2љф уf5RS]Чƒ?кЅК~ЃЭcLчЋ.ЩWˆ—ЋПW–œ§хћЮ—кa <пСѕШ јž§юњбbOомEп€q‹;ЁшиѕЃ-к7*КЖ+’ЩDЏˆЊ3y2Ч.^L"•&ќxƒA,;a“H&Б ’ЉTD&%ЈXpDНіQ?aѕт‡aPю‡зq™жФ43&ќвd%e  JЋ†йvќ?UцЏњY–ХНЗоЪЁ§ћЗшtТ0 sяКžŽРwЉŸS–VQWWЧаaxН юМюY€џ6Ц|ьU—ф+ФKцDd№–ХчEУШ žчФ+qQС$ŒZBэгК9ЧОѕ9Žyg‚оC!ћžајУащu#5=Л4A ддз§ЇяКЇэнО§­Лw3aњt™:{6Е Ба(оРїё\лЖT)Е,QТA–&™”+ŽЅК vљ*+rџ‡)ATЭДPJPЪB,uфdQcЪДіŽ9двЂиёШ=јОU!™Цž ий€lГІf„0|о6=2‚mЋ^)"п7k§ўз/‡:`оc baai Žиš&[ь}VѓшЗf]Ј˜њEюдŽ6˜ЌŒ! FЫі1уЦ=йййљЧBюВ6,z~ѓf>fŒ™6wЎŒ7.ђнћдЈrVnзVЊЬ™…ёш ќˆ ьjoЇѓаAМB1ЎИхI'ˆ­ЉЅКОŽкњzš†Wп”„хяŽІ–Ч™ƒHyAЉžіvžќѓŸ1€J$IІ2˜ў^мBŽъa0щT!л,dCвM!‰ Ÿw|b6з]|HaЬO€EЏІ _)^–(K™qГЅTx uМЁДзЦgњлmъЗlЛW“я04MfŽЕ!aЪцXiП5€|cSгcMMMkћ{ћЇєїї]|`яоГюн›ЉЉЏgмдЉŒž4‰!У‡ЧM"КьЯMlТxжpыюнДэпOчСƒxЎ5RЦ}Ђ$jщŠ'IvhЅd,лbшШQ 3†цQЃЈЎЋ‹J…RЪŠADhнН›ЧяЙЯqЉ2ŒbЎ|з!tв8UГHШ…Њ&HV кv L$?ђЫнmX("_4Ц,фњ’ё’cЅдЮqG7MњФЯO=ЬїGп_ХёяZ‡ƒ_`wWhі<Њ6lиАQЃFЙ~*•вОŸВD2bLUоq†мЗяw–ВЊDЂBR*aм”)Œ™4‰†цf’щ4A<вwmоЬўчwт{T7$ЉšІzH+­1Ђ1Ф=ј:@—кБ‹р …N…гѕЪ‹DщhуАa >œКІ&\зЅПЛ›=лЖ™b>/FыЈц†4UC4ЭГЁzX$јЊ&Hе bƒЁЁv(kтАё]Ь?ёMtЖbїŽ-ГЭЋДТи+ХЫБ#GVХЃ<рЏ­@pЄ8\ј0И“РЬvSSSo__ŸTUUЖRžˆ8a2Yшnk›x^Uї ŠR„AРЖѕыйЖ~}yYЗ’щ7Fгаœeф”дOa”&д†m4AA =Œ 1*Р( ­4E …а@‚hTЦМПФ."R€DP;^А“BїѓАo•ІКІю3Љlf[ЖЊЊН§аЁNБ№нЦP3Ъ bР2иCКВC ‘žО9фР3šLЂa"Д=ЇyѓyяbыњЕќязўƒ[ю~‚c_ЪгO>rОˆ\nŒё^K!џ=МT 0 fHђˆQ^ЖёоЭћќх–<ћŸ бЁЁљhA%„CыЂ)ЫOі>к5јЬwмaГlй2НЋН]wuu…Ёч5hm0Bд™ЋepЉ–˜ЙQdЊ“Мљ=s M€6ATЪГА,Дђ1qгЈ*ќРрљЫf§DКЁСhС„€ЪьjCрkt 7WU7mБь0, uуЂ Ђ QeHзЧ>ПVиИ"фРгš'ЪЦgžфр:‡N9‹ћТѕlлє,W]z6ПОѕFцŸє&ж>ёp8xј5‘юKРKUРї…§BKапсђрзћщk 9ёдГ) №ьS†гоКŒѕkgїУэ 1 ЯqщэюЈ3зѓŒˆPR–-гjзЎцЈ1Dв>.хХЫ5€„œ№Жй UƒzёСŠFhљh„tH v И(_ЃJdЌ)­Ѓї…Qлж†л нл :4ИnО'Љ‹НI’к'АЄ›&0и!U ™:!QН-†Н+5Ч/9“хпЙ…{яќџѓеЯ2eЦ15s.SІЯцБ?пУзoМГєlЯр T€—КDЬA€юC…ВР_ rРќХЇЁ~їЮН{glоМ9аZЦRеmвѕ‚e[ Ч #F •JsьёKиДn5ЎНxdNќњунxnQ€зэE/†—МH”1ЌлѕtЗюы,М яиџД‹eлМћŠСR5  Ы0yњ,D„-{Xzцл1Цd€c_ќ|ЦcŽоŠ1+Œ1ХвфБгšЉЪ&IWл$3ŠdJa%Ѓ\_lЪ`ˆЌ…1бbЁ Ѓ5tlћCˆ; Yўэ[8іјЅЛ…Љ3цb'“Мя#џEˆО‘—и аГ[—_j3(vќЂaТфщGмЧœ…'у{OЎzœCНЉ!бšОЙчІв!ЛxёrV [юЛZ­МЕхг„>лCІЯ:–lumљ`ЫŽŠ)%ЭWЪ"[]C_oДPѕдЃч”ћїNjŒ Œ1їcо %zu ‰Є…JФЗ€ј=P†x9иXи:Ожб:}ћ k~rпПlњUHыS†‰GЭЂ%gАь_ў5ц§ЃlcТ”щ iЁEфЌиEн(BяІ;ДЮ2хдb…"т,њ‹>mН{кѓT™ РкЕkизYрйѕK7ж \ќ№eШрUЧЫАцAрgпСЪŸŒКqb+р uMGя:бЛRщ4~ЈЉЎk ПЗ?д4Žš\ZkіЫИ†<№uЅЄwу{а&,№hё АK‹Eывоhі?эёшЕ‡жј1Gёќ/o8ѓœ‹˜Л`1УGeџочЫчœЗшdeŒ™+"е֘cИФэ3ХGПшu? i]Ѓщкaа!X a§њuь8˜+oЛ[№Шooцk—ŸЬ-W—пїї)cЬЭ֘Ž—zџЏ^nSшЛ0мўЬ]нKnЫ›…ШˆUM‰Vb•'H‹бШХІ­зС ЂЉX~ККК{KM/ы­\Ц#"їЏўгЖ к[{Uѕ{PШ‡ [›У~з!н{=жоф3tјHОvУЏ=~?Иўj~sлMьy~А.sьёKЙїЮŸбгеNCS3C‡„hЌ79cЬDф„ауЛ{Wъ%{W9ˆЖЎ}”џњф$3YТРЇmїf|ЇPєB№ ppяЫ|іЏ ^юМ€v9 јъСMюЇяўwWO<%Њ–єхђьэ(”miя Xшq(К>нm­(eѓƒы>ЭжЇдDhї+ИюЋ='xЧџёGљиЮ–№fОДќЛцЩяА­4_њЏŸ3z|ф‡ЏјФ5ДюлЭЛ‚sпљ~ЦOžЦБ‹–pя?уёGfбЉчиUЅsжі6o‘&р`8‘{ЪЇЖэй2FDEŽЩшЭРŸ€яc^СНОІxХЋ…‹ШBрыР’јwІЬ;…бSч2tЬdZwЌgхo~@Ж~уgžHшЛ<Зњ‡~“1црfљУ—{ўџОzьщЬeз$Fy8^Ч‹f/—~vН"Лжєђа7ћЙєCџ~D  p e78яdfЯ?™O\w3=нн\uоБwЪл9џ#_cѕwё›>АиМ†/‹zЃ№ŠчcVKEфЂ”mйŽgnкўєCG—яэbѓЪп•~э# |~ЏЕv_щЙуѓMDќЇџМћэ-}њдЫŽRуче ЩУ\€‰j[џTФN&xыВїўеїŒ3гЯЙˆ?ќњ66lйNUУH†O˜СжgWБkЯ>6<љР?r™џєxUп б[П. Šь{v`А№^ ("ˆ’ŸmЊD‰=Ѓ† ЧеШ№ЃSЄ›B$уqзЧкi2‡OчіЈч_ v<ѓШЖ„ЇЙы?ї!оњсЏ3uСЌМѓxъо›Ёќ Vc^ќmЭџЧё†М1фе†ˆЄ€“7‹’ЗmЪЏHЗЂе8b}іGTзеG  ЫlЅш>Д‡ы?r6CFOfш˜ЃиПm-нmНOј[РУџLœЏ&ўПP€BЂw /ЦуA.3ФВдEн‘dы‡јNОŸЮ§;Шї–ћ45"1цcЬНQї№zсџKx1ˆШYРћ€I"jœ1ІAdРнќ7№cЂgўн/Ћ ‚ *Ј ‚ *Ј ‚ *Ј ‚ *Ј ‚ *Ј ‚ *Ј ‚ *Ј ‚ *Ј ‚ *Ј ‚ *Ј ‚ *Ј ‚ *Ј ‚ *Ј ‚ *Ј ‚ *xнёџ?^РjЪƒˆIENDЎB`‚(@€ +[Ц ќџ џ™v62OКџG+џ/џ‰lTџтЗџтЛ—џйЖ–џ џqV>џћz3Оџ>0"џ џ џ џ•Mz F3џOХ‰џ@Чƒџ+ДoџJџPC>џэеПџшаЛџ†ylџsaPџr]JџŠmRџ*!џ—)џ=џ ŠvџщЪ­џMC:џРЂ…џv`LџjXGџ1( џ=) =.џoцЉџVЮ’џCТ‚џ0ЖrџАgџ џЌ›ŠџйУЎџ џѓкУџ-(%џоФЌџ2,&џuГХљ џиv9€S3џАgџS,џš‰{џщбЛџXOGџЙІ•џ}pcџ–‡yџcXMџ=›cД‹џnоІџ[б–џHХ†џ5Йwџ"­gџUџ џ џ&!џђйТџџџцЭџџмZ.џŒHџB џЇŽvџуИџVE3џ\I7џ$џgџ8С|џ%ЏjџIџM?:џёиСџQHAџУЏœџ`WMџКІ•џ&"џŒіџbБ‰џoзЂџ]Ю”џLЦˆџ:К{џ'АkџЄ[џžQџЃTџ+џ:0-џџЮИЅџЕ8:џ™Oџ›OџD!џЉ—‡џязСџ"џсЧЏџ`SGџ џA'џAЦƒџ/Еqџ Бhџ)џwh^џ1*&џfZQџџk`UџИ&ўџџџ+K:џ^Ў„џRЌџHЏ{џ:Ўsџ*Јiџž\џ”Nџ’KџJџX-џC"џџ .L8џu;џHџ}@џ=/-џшаЙџџћсЩџџo`RџN+џJЫŠџ8Лyџ(БlџЌbџX.џ8џP(џ[.џ,џx**.DD5 6џџ џ џ џ>jRџA}^џ8|Zџ0Xџ%‚Rџ~Jџ u>џq<џo:џ k9џ v?џ *џ(% :G$џX/џb3џŠGџ;џ џ  џšŠ|џF>8џ+'#џ џUе”џAС€џ1Жsџ!ЌfџЂYџžQџЂSџQџo9џ‡44/ 440>>5# џ џџBA7џNM@џ.+$џџ#@/џ(V>џ$`@џ]:џX3џ S.џD'џ+џџ$#ў32)џџ ў џ2џc3џ‹Hџ–Mџ ŠJџџ†xkџНЩYЯ“џJЧˆџ:М{џ+ВnџЋcџ[0џU,џ}Aџs<џ›881*”ѕHG<џsr^џˆ‡pџ€jџ€iџ€jџts_џOMAџ.+$џ!џџџ(#џ=;2џ`^Nџ|{fџ€jџ„ƒlџ‹ŠrџdcRџџџџV,џ…DџІaџ@Вyџџv8}ZџSЬџCТ‚џ4Иuџ"Ѓbџ|DџN(џP*џ Z1џ гіVTFџ~hџ{zeџ~iџˆ‡oџ‘wџ—–{џ˜–|џ“’xџŠŠqџ„ƒkџ~hџ||fџ}|gџ}hџ}hџ€jџ…ƒmџŠ‰qџŒ‹rџ‡†oџŠ‰pџKJ>џ% џ% џџW-џ#ˆVџ%Z?џjїYЫ’џMЧ‰џ=Н|џ-ДqџrCџ‘Pџ [2џ#џ0.'џ{yeџts`џxvcџ…„nџš™}џАЏџ›š}џ‡‡nџšš~џЗЗ•џЌЋŒџЂЁ„џ›š~џ–•{џ”“yџ•”zџ™˜|џœџЈІ‰џІЅ‡џЈЇ‰џЁ ƒџ“’xџ’‘xџџџ"џ џ%rKџф3 џKДџ:žlџ)‚TџhAџC*џџDB8џwvbџus`џ}}gџ‘vџ€џ9:.џџ523џHDBџ,*)џ џII;џ‚‚kџВБ‘џЕД“џЖЕ•џЅЅ‡џƒ‚kџ]]Kџ""џџ џFE8џЇІˆџ–•zџED9џRF,џRF)џ џџ0d џ#M7џ2$џџ2.(џlkYџ}|gџ}|fџˆ‡pџЂЁ„џ[ZJџџ{yџŸ˜“џ‰€|џ|rmџukfџpf`џLE@џ'!џ џ џ џџ'#!џ=86џYUSџ`]\џKJJџџUUEџž‚џƒ‚lџ џ"џNC'џ џ  eчeЁџ96-џ^]Mџ€jџ‹Šrџ‡…oџŒŠrџ››џuџџLJHџž—”џ–މџ‚}џƒysџxmfџl`ZџaUNџXKDџNB;џK>7џE81џ@3,џ;.'џ7+%џ6,'џ;1,џPHDџ643џ%%%џ џ™˜}џ›š~џ! џDeCџ0H0џ'џћЊ Rџ•џииАџЛʘџЉЉŠџ Ÿ‚џžџІІˆџЉЉŠџMM>џџda`џ|zџ’‹‡џ”‹†џŽƒ~џ‡|vџtmџxjbџo`WџbSKџVG>џK;3џF7/џF=7џGB=џ>72џC6/џA.$џVE<џ.+)џ++*џ0//џTTDџ˜—}џLJ>џ7S5џO~NџTŠTџZZџJsJџ(=(џ џ‚ЈџЈ в оY.џ&џdbPџЌЋŒџМ˘џЊЊ‹џ‡‡mџ>>2џ џHGFџlihџxtrџŒ†ƒџ˜Œџ”‹†џ„џˆ}vџ‚voџ}nfџwf^џq_VџiVLџ^K?џ[RKџUH@џR=2џ[G<џA<6џZ@1џU>1џ5.+џ?>=џRQPџ џŸŸ‚џvuaџ%4#џWŠWџb›bџbœbџ^–^џXŽXџWŒWџ3O3џі5eџ‡EџšOџ›Oџd:џ9%џџGGIџdddџ‹‹џ”’’џ„€џ‡„ƒџ”Œџ˜”џœ”џ•Œˆџ…€џ‰~wџƒvoџ}pgџyi`џsaXџtaVџiSHџfNBџcJ<џ`MAџ`I<џSA5џYC6џU:*џ@5/џNJGџkjiџџŸž‚џ˜—}џџd•cџkЅkџpЌoџlЊlџhЄhџb›bџY‘YџP€Pџ  џD?В:џPџЄ\џ2ИuџaнžџZВ…џ7dMџџџџџџццхџЦХФџАЏ­џЈІЄџЉЅЃџЉЄЁџЄžšџ–’џ—ŽŠџˆ|џ‚zsџ…xqџqjџzjbџ{kbџMIDџmXMџkVIџHA<џSE<џPC;џWPIџN?5џX:*џN<2џVOIџxutџDCCџuu`џ™˜}џ42*џ„œ‚џƒЕ‚џzЖyџuГuџsАsџnЌnџhЃhџ]•]џR…Rџ §Ћжёв‘џџџнm?џ5Оyџcп џ`Лџ:gPџ ў <…­­­џєѓѓџпонџШЦХџМЙЗџДБЏџ­ЉІџІЁžџ ™–џ™‘џ‰|џtkeџ‡{tџ‚tmџ|nfџxg^џMHDџsaVџ`SKџ\NDџaH9џ^B4џ[LBџI>6џ\?0џYC6џYLFџ|vsџ~|џAA4џŸž‚џrq]џIRGџšП™џŒОŒџ‚М‚џ}Й}џvДvџoЌoџeŸeџVŒVџFuFџGoGџPPџNNџPPџ7W7џсZџH˜pџZБ…џ9eOџ џЊG,,,џџџџџэььџждгџУСПџИДГџАЌЊџЉЄЁџЃ™џ•‘џ“ˆџpg`џŠyџ…xqџ€qjџ{kcџNF@џwg^џVLDџjZQџfOCџdK>џkTHџHA:џaG8џaI=џ^LBџujdџЊЄЂџџГВ’џ  ƒџџИЬЖџЌЮЌџ Ь џ–Ч–џФџ‚Й‚џgЁgџXŽXџL~LџGxGџGyGџ&@&џ  џџџ t#!мџŸBœЕЕЕџїііџхффџЮЬЫџОЛЙџЕБЏџЎЉІџІЁžџ ™–џœ–’џqg_џŽ„џ‰}wџƒvoџphџWNHџxkcџdXPџcWNџkVKџiSGџoZPџGA;џgOBџgPDџfRGџo_Uџ™ŠџHGFџƒƒkџІІˆџRRCџŒŠџичзџЮхЮџФпФџИйИџ­г­џ–Т–џ^•^џQ†Qџ&@&џ  џ(џ=\=џ@a@џ@b@џ.F.џ'џ џ~q џџџџџѓђђџонмџШХФџКЗЕџБ­ЋџЋІЃџЅž›џЂ™џsiaџ„{uџwngџlb[џaXQџMD=џwldџzmdџIC=џvd[џo[Pџf[SџD>:џr^SџlXMџnZOџr`VџŒwџИВЎџџРРœџЇІˆџ џ998џ_b_џЅ­ЅџъљъџацаџНмНџšУšџ^•^џOOџџ/J/џK~KџT‰TџZ‘ZџXXџTŠTџQ…Qџ%џ  LPOOOџџџџџяююџизеџУСПџЗДВџАЋЉџЉЄЁџЄŸœџ{qhџˆ‚џ…џ„џŒƒ}џzrkџtjdџ|meџsh`џTNIџWQLџaXQџ`WPџvg^џr`VџsaXџwg]џ‹}uџФМИџZYZџpp[џЧЦЂџœœў$$џ“—+++џхюхџвчвџЕиЕџ†З†џ_˜_џQ„QџOOџR†Rџ_˜_џgЂgџeŸeџ[’[џN‚Nџ-J-џ ƒЁŒŒŒџџџџџьыыџедгџТПОџЗГБџЏЋЉџЉЄЁџ‚xoџЁ›˜џ˜‹џ“ІџŽ„џ„|uџqgaџƒvoџ€rjџogџphџzjaџyi`џxh_џyi`џzkbџskџ›ŠџЭЧФџяэьџџeeSџ‡†nџє ?***џяћяџТпТџŸЬŸџxБxџbœbџ\”\џZ‘ZџiІiџjІjџdždџWŽWџGvGџџ/b/ЛšššџџўўџыъъџждгџУРПџЗДБџАЋЉџŠwџЄŸ›џž˜“џ™‘џ•Œˆџ‡‚џne^џŠ~xџ†{tџ„wpџ‚unџ€skџqjџ€qjџ‚smџ‰|uџž”ŽџСЛИџэыъџ"!!џ6by€‘žЂžџдщдџЉбЉџ‹С‹џwГwџ&џ%"џ џEmEџ>d>џ@i@џ џ!5!9НwwwџџџџџэььџйизџЧХУџЛЗЕџ”‰€џЄŸšџІ œџЁš–џœ”‘џ˜’Žџ{smџ‘‡‚џŽƒ~џŒ€{џŠ~xџ‰}wџŠ~xџŽ‚|џ™‰џЎІЁџЭЧХџюьыџ776џU 9:9џъјъџЙйЙџ•Щ•џ#5$џ‘“uџилЎџжиЌџџTTCџ31(џŸ†>>>џєєєџђђёџутсџгбаџСОМџЙЖГџВЎЋџЋІЃџІ џЁ›—џž–’џš’Žџ˜Šџ•ˆџ—ˆџš‘ŒџЃš–џГЌЈџЩУРџомкџчхфџџQзёљёџЫуЫџ‘Л‘џ)&џик­џвдЉџЯбІџЇЈ†џDE7џджЊџ23(џ%@ё––—џџџџџё№яџхфуџлйиџЯЭЬџЦТРџНЙЗџИГАџВЎЋџЏЉІџАЉІџГ­ЉџИВАџУНЛџвЮЬџуспџќњњџЃЃЂџљ+‰ЋЏЋџмьмџs‰tџwx_џавЇџŸ~џџухЖџџџЈЊ‡џ˜…џ“““џќќћџ§ќќџ№№яџъщшџфутџпомџлйзџйжеџлизџронџчхуџюээџџџџџНММџ111џ–9:9џѕўѕџfqfџ—™yџбгЈџ`aNџџРТ›џ##џџЕЖ‘џСtл@@@џ•••џЮЭЭџџџџџџџџџџџџџџџџџџџџџєѓѓџЕЕЕџqppџџ™ИЕИЕў‡ˆџƒ„iџавЇџklVџџгеЊџџџЙК”џРfœвчњљрХˆFђ‰‹‰ўNO>џвдЉџЪЭЃџlmVџЭЯЅџ#$џ$$џЈЉ‡џ— СџЪЬЂџавЇџзй­џVWEџ‹ŒpџорВџ//%џ"&џ€‚gџRRAџџllVџ//&џzk?Lџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџј?џџџџџџ€џџџџџџџџќўџџјќџџј№џџјр џџјр џџјрџџј€џџќџџќџџўџџџџџўџџќ?џјџ№џР?€@ррџ№џјџј€џќ?€џўРџџџРџџРџРџџрџрџџќџрџџџ€џ№џџџџџџјџџџџџџўџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ(0` XН оЧo < X-  })ыU4џ=%џЙ”wџщУ џБ•{џL:*џџ z “ $юxdTџ@6-џ_N?џ;/%ѓ' ЙЋIŸsџMЮџ2ПwџˆOџ|ndџывНџUMEџ‡ueџЅŒtџ1)!ь(Thz\# )Е”VџT3џвЗЄџncWџБ‹џ…vhџi]Pџ37MnєmтЈџQЫŽџ7ЛyџЈcџZ3џ&џF>7џsfџиС­џиGC#ъq:јBR<ќСœ{џH9+њ8, ѕnfCџ+ЖpџyFџš…yџdXPџЈ–†џƒtiџ93-џ!АџK†hџlдŸџTЦŒџ=Л|џ%Ўiџ ЂWџЅUџ:џџ‚tџ‰) ‹Gџ‘JџHbKџщбМџ@93џЋ—ƒў)!џ9 ,,xQџ7ЛxџЈcџ^;џ2"џ)H2џ8'џЇ ъџџџNnџF˜nџ8™gџ%•]џŠMџƒDџBџg5џ џ (#Ј`2џ}Aџ Z2џk\Sџ2,(џ…wkџLC:џ!  /sQџCФ‚џ-ДpџЂ\џ”Lџ•Lџ~@џ šЇЇ™KKB х џ(("џA@5џ+1(џ9*џ'X?џ"dBџ_9џ W0џ E'џ -џ#џ/.&ч№%џA"џx=џAџ e7џG=8џи-eHцNШ‹џ9Лzџ%Бjџ k;џY-џv>џВ&&!S#"гKJ=џzydџ‰‡pџ‡†oџ…„nџigUџ@=3џ/+&џ,)#џ50)џMI>џsp]џƒ‚lџˆ‡pџ~}hџ*&џ џ%џw<џ#ЇfџM4§-& QМ†џGЦ…џ1ЗsџIџ p;џJ(џяNMAџrq^џ‚kџ”“yџœœ€џ‘‘vџŸŸ‚џЉЈŠџšš~џŽuџ‹ŠrџŒsџ’‘wџ˜—}џœšџžœ€џ•”zџvuaџџ џ&џ$uLџ•!3%яLЙ‚џ3šfџrHџF*џ!*!џfeTџwvbџ‡†oџšš}џBC6џ762џNKFџ861џ>?3џ€iџАЏџБА‘џЅЅ‡џiџPO@џ++&џ*)$џ€iџ›šџ<9/џL@&џџ ъ€џ€U  џ#?.џ'џHD:џ{yeџ‚kџŒ‹rџsr^џ/.+џˆ‚€џ—މџwrџukeџ]SNџ/)&џџ џџ*$"џE@>џ^ZYџ@?@џ))#џ•”yџts`џџE?%џ щ$ @џ@ Оvu_џ‚iџ’‘wџ›šџ Ÿƒџ~}fџGG<џLJIџ”‹џ–ˆџ‰~xџ}rkџpc[џbTMџUG@џK<5џF81џC:3џ<3-џ<.'џPC<џ/--џџhgUџŽuџ6E1џ>c?џ7W7џ+C+ѓ-Иq€Ÿ€,(н)џ‚zfџИЖ•џЊЊŠџ~}eџ66,џ?>;џa^]џ€{yџ•ŽŠџ“Іџ‹€zџƒwpџ{ldџraYџgUKџ[J?џTJCџP>4џRB8џL<2џU?1џ40-џDCCџ--&џ››џGM=џO}Nџa™aџ^–^џS…SџFpFў#ц DfЬf n4џ›QџЉ\џhAџ џWVXџ|||џ˜—–џ‹ŠџŽŠ‰џœ—“џ•‘џ”Œ‡џ‹|џ„xqџ}ogџvf]џp_UџiSHџdM@џ^J@џ[F:џVD9џW;,џD93џb`^џ)))џ™˜}џXVHџ\‚[џoЉoџp­pџkЇkџccџZ‘Zџ 2 џ U€  ЇvFџ8Чџ<Єpџ7zXџ4eLЮ ВЧЦЦљђёёџШЧЦџЕВАџЏЋЉџЉЄЁџ ™•џ–މџwrџ…ysџqjџ{kcџSNIџo[PџZNEџYG<џXE:џQF=џZ>/џTA7џleaџdccџddRџzxcџco_џНŽџ~Й~џxЕxџqЏqџgЂgџYYџ%<%џ'>'ц,F,ў*C*ь#Ђ   ŠH˜oџ>}]џ0%њ  *;;;щџџџџпооџФТРџЖГБџ­ЈЅџЄžšџ›“џ€xqџ‡|wџ„vpџogџUMFџrbYџ^QIџfOCџdK=џWJ@џ^F8џ_H=џh[Tџ—•џ0/'џ­Ќџ//(џДЮВџЁЫЁџ“Ц“џ‰Р‰џrЋrџYYџJ|JџI|Iџ0R0џ"џџzг ГBmЈЈЈџїііџдвбџНКИџВЎЋџЉЃ џ ™–џ†~xџŠ€{џ†ztџ€skџ\SLџuh_џaWOџlXMџkUIџXMEџdODџhRGџjWMџ”‰ƒџZYUџ““wџiiUџ€…€џЧжЦџТкТџПоПџЉаЉџpЂpџMMџ(џ$џ@d@џEkEџ7U7џ"2"џд*$$$ОиииџьыъџЭЪЩџЙЖДџЎЊЇџІ џ„џ…|vџwpџyoiџe]VџvkcџlaYџcXQџcWNџYQKџn_Uџo\Qџr_Uџ‡woџЌЇЃџ`aQџОНšџJJ=џ)*(ъVXVб™Ё™џлялџЏгЏџmЂmџBkBџ_<џ2N1џ ‘#T#CRRRйЭЭЭџ№яюџгбаџЗГЏџЌЇЂџЊЅЂџЃœ™џ–’џŽ†€џ”‹†џ‡џ„џ’ˆ‚џ“џД­Љџнижџ•“’џœ{айаџЛлЛџSoRџŸ €џсуЕџŸ €џPN?џeћ lЎџџџџџъщчџзедџЩЦФџНИЗџДА­џЎЈЅџЋЄЁџЌІЃџД­ЊџУНЛџмйзџюэьџhhgџlD Є єауаџfs\џШЩ џ‘tџ‚„iџ;;0џ?@2џ^^KЫY мЏЏЏїрпрџчццџъшшџчцхџурпџтрпџхттџтсрџррпџАЏЏі%%%з8+,+мсъсџu{hџЯбІџLM=џ[[Hџ>>1џџdePщL###ŽaaaП{{{ф………њ‹ŠŠџˆ‡‡ў~эffeЦ+++–NHbdcџwymџЩЫЁџ†‡lџ€‚gџ56+џ$%џhiSр  C єЎЏ‹џезЋџ›œ}џxy`џЖИ’џ,,#І ТopYяуUUDщ'(з),џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџќџџџџрџџј€џџ№€џ№џ№ џ№ џ№џ№џј?џќ?џјџ№џрџ€?€џ€џРџрјџ№ќџјќџўќџџ€?ўџџџџџџџџџџƒџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ( @ @ |_lЄ‡/ L5Я!yLџ7P8џњзЗџxfUџYF4џgQ/јMeLџЇ“џЃxџgYLџ)L;НiуІџ<Н|џ …Jџb[Qџ‘tџ„viџ|™5цlVDџ,"м j 7Ф}џ^8џƒofџl\TџWMEћFы":.џcЧ”џCК~џ ЉdџЃTџ L*џ6>2џJd3џ|=џСІ˜џ~pџ<3+џ ‚IЮ‹џ%Гlџt;џ@џ-Х› џ&#џ,G7џ+cFџoFџ d6џ R,џ5 џ#œ'џd4џV+џ#@,џB;5џIOН…џ7Кxџ‡Mџ[.џ="н+*%ЌWVHџ„ƒlџŽuџŒ‹sџb_OџMI>џPK@џieUџ†„nџŒ‹sџTRDџ џJ&џ.›eџYA/зHРƒџ#ˆUџ Q-џ>?4џ{zeџ‘vџccQџbbTџ__OџŠŠqџ­ЌŒџЇІˆџŠŠpџ_^NџeeSџЁ „џ,'џ#џ Ь€џ€.џ=B6џnjYџŒ‹sџxwaџOLHџ–ŽŠџƒysџpd^џC:5џ$џ"џ1)&џTNKџ,+,џggTџQPCџ:D+џ  Ы7@џ@2 ђЙГ”џГГ‘џƒ‚iџMLAџRPPџŠ†џ’‡ƒџ…yrџxh`џdTJџRC;џLA9џG<5џS=0џ10.џ883џ…ƒmџ?d>џ_™`џJuJџ'='ыEfЬf †t<џ)Рtџ+oNџRORџЇЇЇџžœ›џœ˜–џ ™•џ‘‰„џ…zsџ|meџl^VџkTHџWG>џVG>џV>0џKA<џNMMџ™˜}џHYEџvАuџp­pџeЂeџCkCџtkS5'ž:›jџ9pTѕ( ’)DEEЎќћћџЧХУџГЏ­џЅŸœџ”Œˆџ}smџslџcXQџiZQџdPDџbK>џTB6џ[H=џŒ†„џhgTџUUGџЉЭЈџŒТŒџ}З}џ]–]џIxIџFtFџ&>&џ– œ/АЏЏџсррџМЙЗџЌЇЄџЂ›˜џ}tmџ|qjџ`VOџuh`џcULџmZOџWIAџjUJџƒtlџ^]Vџ  ‚џfjgџЊЗЊџаъаџ“О“џHwHџџJwJџJuJџ2O2џ  r |ъъъџзедџЗДВџЊІЃџŠ‚{џ‘ˆ„џ‡~wџymfџqe_џlaYџrd\џve[џ„vmџУПОџ^]JџVVEџ8WYWнжюжџДџZ’Zџ^˜^џjІjџUUџЌ###ЁуууџижеџЛИЖџ–އџŸ™–џ–ŽŠџ€vpџ‰~wџ„xqџ„wqџ’†€џбЪЧџWWVчЇЏЇџЂбЂџH_Cџ|zbџ5H1џ$юqЁ ЁџѕѕєџЯЭЫџМЗЖџЎЈЅџЅž›џЂ›—џЉЂžџХПМџнлкџIHHеprpс­ШЎџ•”uџ‘tџ{}cџvw_џ'122ЁŽŽџгггџћњњџјіѕџієєџТТТџ{{zёmwАИВџЗИ‘џџ||cџVWEџ[9gvb,)))Еz{aџФЦџrs\џ€fџ&KGG9Ж%%™ hџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџљ№?џрр!џРР џР€џрџрџ№џр€?|ўўџ€џР?СџџџСџџџѓџџџџџџџџџџџџџџџџџџџџџџџџџџџџ(  )8>ZVGЯ^QDО#8.$:Ёnџlbџ|hZџ  T7D.w K.]Э.zOџ1U<і"џ?fџ ŠLџ@&џ-Й/}PџQLAџ/iLŠ/ЌnџE(јliX№{{fџzxcџ‚}hџ|ydџƒkџ!џU6ƒJso\џur]џfc\џŽƒџbTMџ5*&џF93џ::5џU`Gџ9Z8ХD"[?"‚Qѕ/D<ЙЪЩЩџЇЂŸџ‰€zџtf_џdQFџXD7џ`WSџgiUџŒХџ_—_џ0M0Н  ~.ИИЗ№ОКИџ”Œ‡џ|smџpc\џgYQџxf]џŒŠ{џdh`еАЧА§FpGџYZџ0P0ж222ААЏьЫЧФџЇЁžџ‘ˆƒџЁ—‘џЊІЃџ))+7~€~ŒІ„џvy_џAD5ƒ...]]]eІЅЅБЈЇЇП‰‰‹yџrt\џLM=ЊCEC//&OџџџџџџџџЬˆ€Рр№8ќxџџџџџџџџtortoisehg-4.5.2/icons/general.ico0000644000175000017500000001525613150123225017752 0ustar sborhosborho00000000000000h6 hž  Ј ( –:|†w—\›“QЄœD–\y‘~Ѕ›Y3„гWŠТЅjЎІM]ЦhŽПБЈQaŽЦPжr•Мo”Т­Ѕv,”тІЂŒ~›ПМГdРЗjРИl.ЂъСИy[Ђч1Ѕь5Љь8ЌэЭФЭУвШ‹гЩ’ZНѓaПѓdСѓмг›ЧєрзЁщпАщрБяхК------------------------------------------$-----------$( ----------%(---------- &( ---------&(------#)"-------!,+'  ---------)* --------- --------------------------- ----------------------------------------------------џџџћџсџСџƒџіРРРЬќјјџџџџџ(  6surЭо-%>„ƒ-Ђъ§ZНѓў6€боDА0Ѕь§]ОѓўЧєў&fГО Nœ 4Љь§aПѓў€Цѓў!`ЏЕ  TІЎ8ЌэўdСѓў€Фѓ§VŸЊ '# FNH83f R_KР,”тўgТєўУђ§SžžUN"„гЩ’§рзЁ§вШ‹§ –PёМГdўy‘~ўZЂч§H’3ЭУŒќяхКўщрБўмг›ўЮХƒўРИlўЊ L§-=<}A;rсзЂ§щпАўŸ–_й˜SмЭФ€ўРЗjўwn+У&#@СИx§”‹Oж†CнКБ`ўЄœDў&#@# < (‡=фБЈQўІž@§94h Œƒ6фЎІMўЇŸAў‘ˆ1ѓ .›’8ћЄœ<ў‡0ё/+ P =1, _ џџџћџсџСџƒџіРРРЬќјјџџџџџ( @ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ@@@@++ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ66(>;5­860ћ871љ=;466(џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ333<;4сFF?§=<5ј64.џ971ў>=7Ќџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ@++ <;5еpqmљDC=ѓ55.H55,:982ћ:82і999 џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ+++=<5ХdeaїЕМЙџHGAђ64.^55/R;93њ<93є@++ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ<:4ДZ[VіДЛИџЖНКџ–›—ўNMGяIHC№JJD§><7™џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ;:5ŸQQLѕВЙЖџЖНКџЖНКџЖНКџЕМЙџqrnљ=;6п55+џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ<:4‰JIDєЏЕВџЖНКџЖНКџЖНКџДЛИџbc^ї>;6Я<--џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ:81rDC<ѕЋБЎџЖНКџЖНКџЖНКџАЗДџVVQѕ><5К@@ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ74/]?>8іЅЋЈџЖНКџЖНКџЖНКџЋВЎџLKFє>;6ЁUUUџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ74-J<;5іŸЅЁџЖНКџЖНКџЖНКџЅЋЈџDC=ѕ;:4…џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ62.B;:4ѕ™žšџЖНКџЖНКџЖНКџžЃ џ?=8ѕ:83iџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ55-D=;5є™žšџЖНКџЖНКџЖНКџ”™•џ<;5ѕ660Pџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ73/F=<5єšŸœџЖНКџЖНКџЖНКџŠŽŠў<:4ђ440;џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ55+><5”@?:Ь=<7х?>8б=<6Ћ><6єœЁџЖНКџЖНКџЖНКџ‘ў<:5ю771*џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ<94Ђ=:5јuxsљ–›—џЄЊІџtvrџ…€џžЄ џЖНКџЖНКџЖНКџ™žšџ<:3ѓ72,.џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ65-ˆBA:џМУОџЩвЭџЛУРџ…ˆ„џ<;5џHGBџ“—”џЖНКџЖНКџЃЉЅџ=;6ѕ55-Dџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ66.!=;4ўГЙЕџЯзгџЯзгџХЭЩџA?9џmpmџoroџDC>џZ[VџЇЎЋџBA<ѓ860_џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ970ЖprmџЯзгџ”Žџ‚…€џЧЮЪџ‰‡џ@?9џ[]Xџab^џWWRџЌГАџ:82і+++џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ@?:іЊАЌџСШФџ<;5џ><6ыBA;џšž™џТШХџnpkџ971џЈЎЊџЖНКџPPJ№73/Fџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ=;5ўТЩХџmniџ=;5Юџџџ53.i><7љRRLўžЃŸџПЦТџЯзгџЩбЭџbc_ѕ55-`џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ>=6§ŸЄŸџ;:4ў333џџџџџџ75.Ц64.џNMGџДКЗџЮжвџ>=7ў77)%џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ??9я>=7џ76/џџџџџџџџџ>=7љ™™џvxtџ<:4џ^^Xџ@>9њџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ83.2861ьџџџџџџџџџ77/ЂYZTўЯзгџЊАЌџ<:5ў=;5я::4Цџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ999=;5ўЖМИџИПКџ?=7ў33/hџџџ333 џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ=;6аPPJџvxrџ<;5џ64-’џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ74/ž<94б972Н9//1џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџУџџџџџџџџўџџќџџјџџ№џџ№џџрџџР?џџ€џџџџџўџџ€џўџќџќ?џј?џј?џј`?џј№?џј№?џ§р?џџсџџџСџџџЧџџџџџџџџџџџџџџџџџџџџџџtortoisehg-4.5.2/icons/hg.ico0000644000175000017500000013257113150123225016733 0ustar sborhosborho00000000000000 ш–(~ јІ€€ c0ž@@ (B600 Ј%)x  Јб hyЎ  ˜сВ( @-.-:<;KML]_^lom}}–™—šœЈЋЉИЛЙУЦФдиеюёяџџџџџџџџџџџџџџџџџџџџџџџџѓe‡Eџџџџџџџ2cџџќGм_џџџџџѕFvg_џ{‡TЦџџџџџ+˜юЧ_џќЎмЅ џџџџyˆ­ьUџћžюыpЅџџџџˆˆˆо‘џњююЕ _џџџ˜ˆˆŽЁџџЉююъ!ˆџџџЉˆˆŠЃџџЈююgџџџћˆˆˆџџњˆˆЮЦПџџџЙˆšЯџџћˆˆŒъџџџќЛПџџџќˆˆˆн`џџџџџџџџџL˜ˆˆž ,џџџџџџџџђ\˜ˆˆŒгёOџџџџџGlˆˆˆ‰чљxbџџџєJІjˆˆˆˆъxŽчOџєeR9Јˆˆˆ‰ЬˆŽь?џ@Š˜ˆˆˆˆˆЋ%ЈˆЙOѕЙˆˆˆˆˆˆˆŠ7њ™‹џPšˆˆˆˆˆˆˆˆš(џЫПџGЈˆˆˆˆˆˆˆˆš/џџџї;ˆˆˆˆˆˆˆˆˆ‹?џџџѕЉˆˆˆˆˆˆˆˆˆ—oџџџјЙˆˆˆˆˆˆˆˆˆЃџџџџјЩˆˆˆˆˆˆˆˆˆ†џџџџџЩ˜ˆˆˆˆˆˆˆš_џџџџџКˆˆˆˆˆˆ˜ˆ–џџџџџџЛ˜ˆˆˆˆˆ‰™џџџџџџћЙˆˆ‰ˆˆ‰›џџџџџџџџЛЉˆˆˆˆŠПџџџџџџџџџЫЊ™™ЋЯџџџџџџџџџџџћЬМџџџџџџџџџџџџ€џќ€ј?№€№€№€№Р№РјрќрўрџџРџџ€‡џƒјрР€ƒЧўўўўџџџџ€?џРџ№џџўџ( (+)697KMLZZZkmlƒ†„’”“—š˜ЄЇЅЏВАЛОМЦЩЧбегрусђѕѓџџџџѓ_џџџђ4џ4oџџgл?›йџџwl?љю‘_џ–v_ј‹ф?џљ‰џјg}џџџџіwkCђ/џєVgvАkЯё$XwvБ†‡wwg‚џџHwgwv‚џџ—vwwg_џџ–wwvxOџџІwwguџџџљvgwŸџџџџљ˜ŸџџџŸуСССу€џ€ž№№№№јў( GJHUXV^[]^a_gihutu~‹ŽŒ–™˜ŸЃЁЊ­ЋЖКЗШЫЩдзе№ѓёџџџђ/џџXo˜rџјŠ§ч/џˆљН/џџџј‰t№џџhˆ ŒџxˆЁљђˆˆˆ’џљˆˆˆџњˆˆˆџћ˜ˆ‰џџџњЊџџўpФ0†ЦўМ0 ррр0ј№‰PNG  IHDR€€У>aЫ IDATxœэw|ењџпГ%}гћnHшDЄ%є"ЋQPAС.ЂxUф+Wёrљ)рUЏŠQёbA Š !CІв;!РnzBHпl™пЫŽйd Є€—Яы5ЏdwЮЬœйѓ9ч<ч9OыјŸ† IRkзЁХ!ŠЂшєК€ї…УЧХџ^@dКј{FЏзWЗр+4ўђEбш†НБG7ькT€г@Аѕz}UоПй№—$€(ŠюРРр.РГ…ЋPlТN†uzН>Ћ…Ÿп`ќepaXП{Ѓп ј7ф:ЋеŠйlЦfГе9ЌV+’$ЁT*•J%џпHР`5АHЏзŸПЬWl\ѓE1˜ŒТ.Vжl6c2™0™LTUUa2™АX,—§lA№єєt:AИи%ч…Р|Н^_xйnB\ГEбј'0№­Џ\ee%%%%”••aЕZ›ЕN‚ рююŽ——wwїњŠ–‹€ЙzН>ЇY+u \sEQ< ЬТ]•ЉЊЊЂДД”ввв+ъсW ќќќаh4( WEЊ€O9zНўlЫжЮŽkŠЂ(Žц\ЏЈЈ   €ЊЊЋKW(h4ќ§§ыLР `ž^ЏoбЙ&pAР{ћ\_g’­ЊЊЂ  €ŠŠŠЏ[cЁбh AЅRЙ:ќMЏзŸiЉњ\ѕE1дjЕ&*•Ъ›jŸГX,фччSZZкUЛl‚@`` Ў„ЦѓРszН~y‹дхj&€(ŠЌVыїJЅ2ДіЙЪЪJВВВš]АkNЈT*BBBаh4ЎNЏўOЏз5gЎZˆЂјˆ$I_ ‚PgБ}юм9 ИZыоXјјјюJPЬеыѕ››ыйW%жЏ_‡››лZE­_D’$rrrЎЙ!П!pss#""Т•X <ЂзыПmŽч^uHNNюeЕZwЙЙЙЉkŸЫЪЪЂЌЌЌ5Ње"№№pWS‚ЏзыПhъgК\œЖDQŒЎЎЎоъЊёsssџві.;;›ММыЊ!Р™3gОёёёЉЃЫ/++ЃА№ЊP›З(JKKeŒ3†РР@&L˜@ffцыЂ(ОлTЯЙ*0{жьћЃЂЂ†зў^’$ђѓѓ[ЃJWJJJШЭЭрџј:t`ъдЉфххMEёэІxFЋ Ђ( ƒЁXЇгейа),,ќŸь§ЕсяяOhh(еееL:•ѓчЯГpсB4ЭXН^ПтJюнъ#РЮ;Йj|ГйLQQГъ@ЎSXXˆ››oМёеееܘ1“ЩєЉ(ŠёWrяV%РK/Нzу7>эъ\QQб_Fбг(,,ЄЂЂFУЬ™39vьѓчЯїжˆЂxQ;ˆ‹ЁU `БX>ѕёёЉSI’ў’Ъž+Evv6‹….]К0aТжЏ_(ŠQРjQы,‚V#€NЋѓmпО§WчЪЪЪАйl-]ЅЋVЋ•ььlxрШмЙsЩЫЫ |p9їl5tщвeBllЌЫч—””ДtuЎTVVRPPРєщгQ*•ќћпџF’Є Ђ(>еићЕzіъ9б•§œ$IзФО~kЂЈЈˆЊЊ*4 O>љ$ћїяgХŠsEQti%UZ…:­.К{їюэ\ЋЎЎО.ќ5yyyŒ9’vэкёХ_pцЬ?рНЦмЇUрсс1NЇгЙКрsIД BCB]u‚]t ƒеj•ТgžyЅRЩ‡~ˆЩdъLoШ=Zœ:­.,,,ЬЇЅŸћWEqq1fГ™ШШHШЫЫcљђх/‹ЂиёRзЗЦQяЩKxж\‡ œ;w€GyAјц›oШЪЪr\ъкV!€››[Н'ы“ ЎЃ~”””`ЕZ‰ŽŽf№рјЭf>њш#€;DQŒЛиЕ­ёkЗ5Wз?Я_›Э&ЬŒ;€элЗsњєiАЛЮе‹ж €хbKН‹зQ?Š‹‹‘$‰Ю;гЛwo$IbхЪ•ˆЂSпu­A€ЪjГЎ$~–ЎЯ_‹Eо@sŒЩЩЩфцц*Б;бКDырТ№ёЧз9щюю~]ИL8іPzіьIll,VЋ•UЋV<%Šb ЋkZ…ŽљъфЩ“lпОНNыЃРхЁВВRі”КѓЮ;јљчŸ9ўМ7№wWзДЪгOІаЁC>ўју:ю]ооu ƒЏЃ$‰ђђr†ŠJЅТd2ёУ?LК/Щ ­A€C™™™X, РйГg”сыы{}5p™pјNh4њѕыРЦBQЕЫЗі›ЭцЪЬŒLzіь‰——Ÿ}і™ЌвP*•јњжєу:.‚ђђrй˜цж[э{BƒСБ$МЏvљ'€Сh0ПŸWXXшиЮьSVЋmhhжырO( ‚‚‚{Н3gЮшхъ`уЦ‘‹e‡BЁіy9##ƒєєtВГГ9wюeeeœ?žsчЮQXXШщгЇ1щS#Тk!!!‹PЯЮœ9У†_60dШкЗo/ŸHJJbУ† ђgwwwДZэuЁАЈЉ]ѕёљгыШ‘#mEQ €ZHMMэЈP(іЉTЊžfГƒС@NNNƒ,uЫЫЫ9{іЌl<ž’’ђТ%.[ ќОjе* јћпееяМѓŽУШАяDFF^з65#Єж$РБcЧџЦC-фцц~ЅT*CЮŸ?OFFЦe9hфччЫ6ы‚ ЬEёіњЪŒ№„ЩdЊўє“O‰‹‹sm6ЏПў:Лvэ’Пѓђђ"**ъКLp дG‡ў…к˜6mк3!!!}*++bд\Š‹‹BœrяоНЋ/f`0Џ>|Q™8q"ЁЁЪ‘‹…YГfБoп>љ;ЂЃЃЏ+Š.‚њpњєiGл: 66і‡ѓaSxцфццbЕZйИqЃїњѕыч^Ђј`їŠЏVPRRТ”)SœNšL&ІOŸЮКuыфя”J%:NpЎУ’$Щ{5Зз-‹#шЦŸxљЅ—пlлЖ­oIII“EзЖZ­TTTаЕkW6lиpїкЕkыэЎЃСоЗ…„†4Ъ%ЋsчЮ 6ЬIИp C‡ДkgwќUЉTђ”b6›Ѓroƒб№>№ЏŒŒ >ќрC^§uyЏ &l6IIIŒ7ŽЙsчЪбДќќќhлЖ-:__пџнAЛvэœЖб@€ }€GнŽ‰‰!>>ўЂ?j›6mЛoџў§ИЙЙ5И FУ,рЃуЧ3ї?s™:u*У† sYжbБАvэZ}єQfЭšErrВќТ^^^„‡‡гО}{"""№ііўKыBCCДЅŽwul;P#є^А ‰H С’=.YЦё`777ЖoпЮРQ*•]ZLgЯž}цЕйЏ1aТ:wюЬgŸ}FEEZ­–мм\y„ЙR•ддTT*ёёё 8јјxДZ-Fƒ$Irв(ЧёWpIїєєФлллЉэP;жb Ё0HUYY)UWW7HxђёёiСfFFjЕ•JХСƒyф‘G8rфHх%/Ќ JЂ‰:­n{eeхЧ ,№КуŽ;XВd ‹/&%%…QЃFбО}{ЩЬЬ”ЏЕX,ькЕKV …‡‡O||Рз'Nœ`ъ”Љœ8q‚їпŸбЃG3~ќxўёЯСƒйЛw/яНїwп}wлBI’HKKcхЪ•L:•QЃFёц›oђЧ I’œрIЋеуRаНfш ЌcD7 uЪ:@Bк“КХОс|QсЎЊЊЊоaБXE‘‚‚"##yяНїАZ­ЈеjќќќLѓцЭЛЂ„ЃЁЬ`4<Ш'Ÿ|BV–=гЋZ­&22NwБ€­Gz:јГ“^lЈБт VOŸ>MZZ*•ъЂ&XUUUѓb’$‘‘‘Сњѕы),,DЋеВnн:ЖoпЮK/НФŠ+№єє\}ХoyЃс  #№JYYYЩз+ОцЕйЏЮ—_~Щ[oНEaa!ЃGІM›6МњъЋјћћѓт‹/ђгO?QXXHrr2“'O–—ЋАbХ ќq>њш#й(ХЫЫ‹шшh]zWЕ:jџЏЋN> аFjгcbbbfЮš‰››S^^^ЇЧЋT*ДZ-~~~”––RPP@ii)^^^DDD№нwпБhб"њєщCЗnнјќѓЯ‘$I'I’БЉ_\ЇеЏuXXƒbа A( жЎ]ЫцЭ›1мrЫ-Œ?ž{юЙЧЉ79r„яПџžЅK—rт„Г˜тччЧSO=Х]wн%wюмЙЋ.‚yŸ>}dгЏДД4~џ§wЂЂЂ№єєфСЌSпЖmл:кЅD$ ­Vћœ€АА_П~Lz~’\аac^^^NyyЙKe‘~~~xyyБxёbVЎ\I=xт‰'˜цЭ›‡ŸŸ_ЕХbщZRRrВљ†КаiuиSЩ? єЛАдЛwoњшOЗnн8pрkжЌсРŒ3†—_~Yжb‚]…њрƒђѓЯ?гЙsg'wv›ЭFFFFЋц&{cж”eОћю;||| dС‚|џ§їuЎIHHр•W^јУ)a„VЋ~ТњіэЫСC9yвuћХЦЦ2hа † Ц­ЗоЪЎ]˘ќ№CЇћфццЖZFГ›oО™ˆˆ?CюЅЄЄP^^Nxx8пџ= 8G‰ѓііFЁP№Т / зыVИдњH’d3 KеnъNв4Гйœ~ЎшхX-Vд*5ююю”–”кrssфЬАIЖЈkЅё Fƒd06Œ=v"l9АџГgЯц№сУ,^МГйЬ]wнEuu5jЕкeCЗ–ІапппЉё+**ШЩЩ‘“NnкДЉЮ5БББ”––ж”Ю]дДієщгUРЛРЛmлЖѕЗšЌн%$/I!K’tРh4^ѓБн FƒшuZ^’ЄO~ќсЧi'вxюЙчјyнЯtшаГй\g[h5 allЌгчŒŒ  ^^^ффф8ьџХЈЈћhƒmЋOŸ>] Є^Вр5 ƒб ъДКxрѓЃGо7уџЭ`ЦŒx{{Г`С—Ј­!84“5qъд)|||ффd—*ћЎ]Лrј№сšъўƒзыkС`4”їыДКЩч‹ЯЯ™ћŸЙъзўѕБББNЦЈ‚рdљдшмЙГгžMaa!%%%rЯЎgСFC›6mjЋПўoиJ] FУ|рџђђђX0элЗЇWЏ^.ЫЖЄ•‘Z­ІCgGьєєtT*^^^œ:uЊЮ~ РэЗпNJJŠL€ъъъBН^ю:.ƒбАxяиБclјeCНхjNзшкЕЋ“СЋеj%33Sў\ѕ~€ЛюК‹_~љEіПДйlс*Hy `Аy§њѕ.КІFsУлл›Ю;;}—™™‰йl–Эн\@­VHQQ‘<ИЛЛя‚ыИ$ Fƒј………ьмБГЮљ–ь§qqqNBЇЭfуаЁCЈеj<<<8tш“uДC‡eћіэШNAЎО 11ёЊ `0v[їэпWч\mЏ›цBhh(QQQNп:uŠВВ2YГWп№џф“OВnн:Їp1Р^€V[ˆЂ,Т=6›­› Б@—(Šf L„RI’В­‚ Є*•Ъ_|юЗmN|•=Єі—ЎtM Aˆ‹sћoЕZ9tш‚ ШЛД)))uЎ ЁџўьлЗ_|бQч‚;юИуД0DQTI’tЇ a„$In.$h5 IRаш'IвЋеjE1I„w†КЕ%ы}{ВВВ$ЩIъo‰‡]КtЉctђфI***№ѓѓCЉTђэЗпКtэ{ъЉЇHNNFЉTrгM7PVVі“у|‹M))) РA~ю‘$Љ^;tWB•$I `„$IЉЂ(юиМyГkO‘цУ)Гйьднm6[Гg9 Њу‹aЕZ9|ј0˜L&Gn'( &L˜РКuыˆ—э!CBBф}эfЖnнbЕZпсбкчЌV+eee”——S]]еj•…*Apss“??Пš;_§ Х/Ђ(~LбыѕЭ>Œ[ЧЯ‚ ‹сЭ=ќЋеjйМ­&Ž?NUUоооИЙЙ‘˜˜XЧ`ј№сГqуFž}іYЪЫЫKFŒё›ЃLГ 99ЙЛBЁи$BHЭяKKK)..Ошш№рqєАЂЂ"|||ЈщŸ№, EёaН^Ј™^C†ЇЇЇ“cDsџ}ћі­Гйd6›e=ПППНН`тФ‰,YВ“ЩФ!vёЅММќЇšeš)))§$IJAЖЕЊЊЊ"??џВzŽ#Ѕ|ii)5­—ЛЉЩЩЩњ„„„MS{з ёЋљЙ9G€Ž;жбїƒ=Ф‹У“Ылл›еЋWSTTTЇ\›6mИ§іл‰ЅOŸ>ВЩ[hhшG5Ы5‹ АeЫ–!VЋ5UЅRyƒНёrss9sцL“ќhEEEфффдм№P*•П4&XecЁгъэлЗ—ЛЃйlnЖ%`LL Н{їЎѓ}UU•ф) €ъъъz{џ /М@RRщщщмrЫ-TTT”;j–kr$''‡UVV&ЉT*YШЫЮЮnrЃ‰’’’кцXсРЦфффА&}аŸшкЎ};yФl.#˜˜ рraЯž=X,ммм№ѕѕeэкЕŽp/NhлЖ-'NdоМyxyy1pр@Š‹‹дыѕNл„MM!//яWЙчF'ыІDyyyэ{Ч(•ЪљЭё,ЕZ=Јц|cЃЉ4kќььl222ЛIžйlvЄŒЏƒ7оxƒЃG"Š"wп}7žžž˜Эf)88ИN Й&%Р’%KFDDШУpNNŽЫ-дІDAA’$‘ŸŸяјAnLіь†b№Сї;4nхххMК§ЋT*щж­[НoБXјэ7Лрющщ‰?џќГЫоט1c˜?>jЕšћяЗG…=vьиЮaУ†еБ`m2<ћьГ‘‘‘‘ŸЫЫЫыИ%7ЊЋЋ)))!$$„~јН{ї|”””дdІ::­Ю­gЯžВА){dd$#FŒ [Зnѕn+РёЙЈЈЈEгР;М–{ѕъХЦљўћя;vьп€/šтў}њіyбЁ„*))iPЬ…BAtt4јњњтууƒOЃтI’ФЎ]ЛАйl‚@pp0ЧŽ#11БNYAxчw0™L,ZДˆ›nК NwбоM4єъйkžПППрЈtkИL™L&nИсО§і[ЊЋЋoJMMНtд‹K@Їе…ѕъйыЧч†Hџ:Ž#F0`РКtщBddфeЋJKK“чљРР@AрэЗпvЉ*;v,={іdХŠфхх1fŒ}МXя‡& €VЋѕ МлёЙfжЊцDpp0}њє‘?›L&ТУУЛoRR’`ЕZЛвчєяпZ›ш6и5‹'фяяЯ­ЗоЪM7н$[ш\.*++х[ВtщRЇP85Ÿћілv‹ќљѓчGчЮ/йћЁiF€С;u”ЎцZђе„RЉЄџўNaml6юююђојж­[ЁnЄЩFЂoПОOВHл IDAT8ўПиШЩАaУъ„jЙ\ькЕ ГйŒ „‡‡sђфIОўњk—e.\ˆVЋх›oОсР<ђШ#Ž{lИXя‡& @``р0‡ЪR’Є!@їюнёѕѕEЅRе1ЫvЈˆ:„Хb‰uu}CёрŽŒћRЌО%mLL 7нtS“E0?|јАЌф AЉT2gЮ—жGїнw>њ(•••L›6N:OffІ9i]вУ—zж (0шЧђХbБ4ћ№ф=ДіМъh$“ЩФёуЧл\Ab+zєь1зAАѓчЯЛў:uъФР›,enn.иЗ4МММ№їїчЋЏОrщЄ&{,Я;—3gЮ№јуcЕZйœМљ…mлЗ]RЛЂZ vмшјммЖёŽЁПцzЙfЏ“$Щiy”‘‘Ё0›Э—Е?pїнwїŠ‹‹ыфИЏ+сЏ]Лv.uі—‹ЪЪJЖmл†$I(•JТУУЩШШ`йВe.Ы/^̘рр`ВВВxћэЗщлЗ/$55ѕрв/—~аg^BBBМе5l”››7оxcMЯV'ЈT*ŠŠŠœъPQQеjНЌwьбЃЧGŽЈeeeuоЭЯЯЏIпfГёыЏПRUU… DFFvХŽЋпѕ‰'žрюЛэВїЫ/ПŒЩdbвЄIF‹˜"6иXцŠрЗOЭ“ЭS/00АŽ?\M=OOЯ:Бp*++Q*•оДПYГ6>>ОŸуsmЭŸJЅb№рСMšЕdџў§r§УУУёєєdЮœ95SМШˆŽŽfў|ћ–ЧяПџЮВeЫИџўћ‰ŒŒDL_н’КЅСёkЎˆw‹Іцг\~rŽУЕчйšБT*•ЫрM‹ЅбшзkЁCЏaГйъ~єюнЛо‘шr`0ф№їСССh4>џќsGжo'‚Р’%K№ѕѕЅККš &ШИqуиіыЖуŸ|њIЃ<ДЏˆ6›MQ3XRsфёQ*• 2„€€€:чfP !G]NœZ­wwїF)юuZW—.]фФEЕ#Ѕе /w%ШЭЭ•eњњњHRRRНѓўѓЯ?/Чš={6ћіэуЙчžуLцѓъеЋoiьѓЏˆЇ+**ЌŽQ Љ рщщЩ!CœќрkТУaэВqуFAƒ\g4ЈQ;RНzѕzОcЧŽВ\S{CЫЁml dee!Š"VЋFCXXќёsчКЮА3pр@YсГ}ћvцЬ™УmЗнFїюнYЙrхSћьotМš+jБC‡UыДКSYYY}}}х№$Wj)ЋRЉшмЙ3]Лv­—TEEEЂVЋбh4ьоН›ДД48wю*•Š№№№К†ђ—@ž=žsЌ2qƒаh45}ыЏgЯžeлЖmиl6ќќќ удЉSЬš5ЫхzџЦodэкЕxxxP^^ЮИqу тяџ;Ы–-[š"ІИ2.ІXМлЗїO™+‰˜сыыKяоНЙїо{щбЃЧEGЧZ988AфэбgŸ}–ддTzїюMhhшy~чNЛХЧЧkŸkџ]КtiOрєєtЙё Ѓ  €щгЇЛ44ŽŽц—_~‘ЇС^xSЇN1cЦ ~љх—ЃЛvюzђrыrхј§з_•@—ЃW*•єъе‹#FаЉSЇKN%iiideeсссFЃсФ‰ьйГ‡шшhЂЃЃЩЯЯg№рС6›ЭцкWЊ 4шѕšВFэх畆Š-++#%%Eос !88˜ввRІOŸюRˆ aУ† ђВpнКu,^̘‡zˆВвВВŸ~ќщІ Ёѕ/ WLЅEљUqq1ьQЊCЕZЭАaУˆmPя2Ѓ9FўђхЫ˜,, OOOОќђKЖnнЪаЁC5jгІMcмИqИЙЙMКФ- P(юымЅГ“ЗRэ$d2™8}њ4'Ož”u ООО„††ЂP(HJJbўќљ.wнммXГf ёёё€=2щэЗпŽŸŸуŸЯ'Ÿ|2yпў}лU!hБн†mщюнЛ‡_вц9я VЋёііІ]ЛvrN‹ХBiiЉгМ*IЇOŸЎђФММ]7Bђ%еЏіw‚ &ЇІлЙs'qqq§fГ™ђђryERXXHqqБгШЇT* ФппAШЫЫcцЬ™В™Wmh4–.]*/eЭf3=єGeіьй,_Оќгн{vOkьће—"/Z­іnс‡QЃFёРƒИ,cГй8ў<………N?R`` ЈT*Ьf3ХХХœ?‹Х‚ h4ќќќ№єєЄККšЏОњŠ/Пќ’рр`y^Нчž{˜>}:yЙy?~ђщ'ЃSwVз~ФШ'V4ЕQ\\LAAA†НXp‡эОПП? …‚ттbжЎ]KbbbНFЅ;wfЭš5ВЪЛЄЄ„ћюЛ;w2}њt’ж%­ЮШШxа`44™ЮНЩ47FЃёЧ(mдї?ўјушЈ6QrжЯЊЊ*ŒF#экЕCЁP€ŸŸTVVRUUEqq1EEErФ-•J…‡‡‡“еjeэкЕ,]К”‚‚КwяNbb"еееŒ;–ЇŸ~q‹јрeT?іbЉg§§§ёёёЁЈЈH›_ЛёЮЌоооh4Y^8~ќ8п}ї)))Е“=z4K—.•=x wоy'љљљМікkЌљnЭњŒŒŒ‡›ВёЁ‰}%Aš„DПE-Š№є№ЄGЯќїПџхћяП'00Н^OBB7мpƒмИ`Ÿ љzjУjЕ’’’ТчŸŽС`@­V3{іlfܘсXюqћэЗeYВdЩЭ'OžМ-”пЅіѓs8и—‡‹EЎЗZ­–7•––В{їn:Фž={ъъhлЖ-sчЮ•‡|Аo 1FУДiгXВdЩЦЂТЂЛ/„ЋiR4йр@›№67к”Ж-nnnSІNСзз—3f8Ѕ. ч–[nЁ[ЗnrЖ.oooМММЈЌЌфшбЃ=z”#GŽpтФ L&ююю<іиcL™2EЮ8eЪњіэKп>}ЅЅ_.}єЬ™3Ўх/V7q№С=ѓЬ3.Ялl6233х`Ь*• A8wюEEEQPP@ZZgЮœiаЎЈ——/Пќ2SІL‘——‹…їо{з^{оН{3|иp–,YђEeeхгl7=,nЪCЇгѕгFjKЃtQвЄI“ЄŸ~њIКр“жш#((HzѕеWЅмм\I’$)--MвыѕRDD„єњыЏKЯў§Y‹VЋ}рJъЋд>4hр )%%ЅЮБbХ )::њВъюъ “^}ѕU)++KЊ‰элЗKнКu“пwъдЉ’NЋ{Ѕ9кЇцбl7жjЕ§Е‘к mЄVJИ%AZНzЕ4yђdЩззї’?R@@€єаCIK—.•***$I’ЄœœщпџўЗфчч'=љф“вТ… ЅО}ћVъТuw\q]#Е7Д‰j#%&&ж!@ЗnнšЄсћіэ+-[ЖL2™LN юм9щ™gž‘T*•єРHЋWЏ–|рAГ6RћHs7О$IM?дDћіэ§LUІ‡U*ƒ‡ &!!элЗѓЭ7пИt›4i/М№JЅ’­[ЗВeЫЖlйТ‰':t(їŒО‡]Лv‘ššКџ;{іьС+­ЇNЋSEУ‡з<6юOKђ’’Fjд‚B†JЅbШ!r†ѕZКXЙr%“'O&44”Щ“'SYYЩтХ‹ я3 [\мЖЩбЌp@ЋеŽfЏттт0U›8~ќ8ЛwяЦh4:щ”J%AAAr–O…BСž={8•~ЊщFЃqБд„•зiusмннЇО§ЮлВŠьVЗЎ0€]­гщ !$$„ааPBBBˆ‹‹“ЕvЎpъд)&NœШoП§Ц3Я<УЭ7пЬЊoVёЫ/ПЌ•$iМСhЈьЏ™а"{ ]„nД$HгYщJЧNёеиЭМm’ w7wЪЪЫШЮЮ&;+›ТТB$Iк#HТ—’BZn0Ќуo(tZ]8х>kі,Y0[Еj‹-ЊSў­ЗотŸџќgƒеУVЋ•M›6БtщRжЌYУ№сУ?~<'Nœ`хз++ГВВўi0ўл”ядДjBЇгul‚^Є›БgыjюUKHi‚ “$щ ЪІZ™yИйыЅе§јАgЏžLš4InмїпŸ5kж8•-..nYи‘#GXКt)Ы—/Ї  €„„юПџ~ЌV++П^ЩёуЧwŒ†ДfxЅKЂUPЇ‚ DFFz^ …Тн`0фH’дr1Xk@Їе}<ЉфљчŸGЇг!IoМё›7o–ЫЭš5‹W_}еЩ2;;›§ћїЫЧоН{9vьAAAŒ=š‘#GRXPШкЕkљ§їпЏŒ†&ЫЊz9И*p5AЇеЉ9Рd777nЛэ6юИѓќ§§йЛw/_|ё…lŒ"‚l*‚“ѕPLL §ћїЇ_П~DEEБcЧRSSЩ2fБ'Й\вŠЦт:ъNЋ{јQЋе 0€ю=КKVVщщщффф““ƒХb‘РааPкЗoOЩљŽ;ЦбЃG9~ќИdГйRЯ€Dƒба2†€ыИtZ'№0cЖ…G„ˆЏЦЏA(-)ЅЄЄD^uaЏр8№-№ЙСhЈkёqр:€ )цКcЯ.6;‚€@ьБѓ/yР!рW`›СhИКВL_Чu\Чu8сџѕгЉ_’$"IENDЎB`‚(@€ N­ь212џ@?@џ535юО {џџџрONOџžžžџfffџ)))џџ%%%џEEEџedeџpppџ878вN<zˆˆ\  њœœœџДДДџџџіііџџџџџџџџџйййџЁЁЁџFFFџAAAџ€€€џIIIи.! !џTSTџDDDџ989џ///џ(((џ=<=џCCCлU­ŸŸŸџШШШџџџџ```џ˜˜˜џАААџЮЮЮџџџџџџџџџ§§§џ‰‰‰џGGGџ‹‹‹џ*)*ˆ>њtstџKKKџџџџcccџЕЕЕџЎЎЎџcccџUTUџLLLШLKLџУУУџЦЦЦџџџџџџџџџTTTџНННџџџџџњњњџSSSџdddџbbbкI212џ‹‹‹џџџ555џIIIџVVVџHHHџџ џœœœџеееџ@@@џcccх###  („ƒ„џПППџПППџ———џDDDџBBBџ@@@џ+++џџџџџџџ   џџџџџУУУџ444ўgfg№;<;%,+,џТТТџ^^^џ{{{џТТТџНННџБББџЊЊЊџБББџОООџНННџNNNџ џйййџ@@@џд ™™™џПППџПППџРРРџНННџЏЏЏџЋЋЋџ­­­џЗЗЗџГГГџ‡‡‡џ222џџџџ&&&џоооџџџџџ>>>џeeeџ DCDйДДДџРРРџЛЛЛџВВВџџ–––џТТТџмммџеееџСССџџ­­­џЕЕЕџџЌЌЌџ,,,џƒƒƒwŽŽŽџПППџПППџЏЏЏџ•••џМММџЦЦЦџИИИџЃЃЃџ’’’џœœœџИИИџВВВџ000џџџџ­­­џџџџџTTTџaaaћ212BmmmџСССџžžžџ———џ˜˜˜џ™™™џ”””џѓѓѓџѓѓѓџѓѓѓџєєєџїїїџОООџ˜˜˜џДДДџџДДДџ999џ†††ЮПППџОООџ———џПППџѕѕѕџѓѓѓџѕѕѕџїїїџђђђџЭЭЭџЄЄЄџœœœџУУУџaaaџџџџЂЂЂџџџџџ???џqqqє /./ЎОООџЎЎЎџ™™™џ™™™џ™™™џ™™™џ™™™џЉЉЉџяяяџєєєџѓѓѓџѓѓѓџѕѕѕџЯЯЯџџЂЂЂџџKKKџ™™™v"!"mlmkУУУџЗЗЗџ˜˜˜џОООџєєєџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџјјјџвв⟘˜˜џГГГџ”””џџџџКККџџџџџ$$$ў‚‚кMLM№ХХХџžžžџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џЛЛЛџьььџєєєџѓѓѓџїїїџ­­­џМММџџaaaџOOOЦЊЊЊџНННџ———џІІІџїїїџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџёёёџЂЂЂџЋЋЋџ’’’џџџџЮЮЮџчччџџ„„„ЅgggџПППџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ•••џгггџєєєџѓѓѓџлллџžžžџ‹‹‹џџBBBџ”””ЂСССџЁЁЁџ–––џнннџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџђђђџЅЅЅџЋЋЋџwwwџџџџыыыџ‘‘‘џ___џlklT€€џКККџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ•••џсссџѓѓѓџђђђџ“““џАААџџ111џџџџ???&ОООџЏЏЏџ™™™џЃЃЃџѕѕѕџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџœœœџДДДџWWWџџџ)))џџџџџ,,,џЂЂЂџ >=>}|}џЛЛЛџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ   џѕѕѕџјјјџ———џРРРџџ999џџџџ    ЦОООџ˜˜˜џ™™™џИИИџїїїџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџфффџ”””џНННџ***џџџ………џсссџ%%%џЄЄЄФ………тРРРџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џПППџњњњџ”””џТТТџџWWWџMMM___BУУУџІІІџ™™™џ———џРРРџіііџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџєєєџЮЮЮџ———џВВВџџџџуууџZZZџ•••џxxxWџџџvuv•УУУџЂЂЂџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ•••џнннџ“““џУУУџџƒƒƒи›››юЗЗЗџ˜˜˜џ™™™џ———џЖЖЖџєєєџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџїїїџВВВџЂЂЂџrrrџџџџџџџџ###џЙЙЙџ656%ОООџБББџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ–––џ———џЏЏЏџ џгггw~}~yФФФџžžžџ™™™џ™™™џ™™™џšššџЭЭЭџцццџєєєџјјјџіііџѓѓѓџѓѓѓџѓѓѓџяяяџ———џЙЙЙџ$$$џџџџaaaџŸŸŸџЂЂЂzЅЅЅРСССџŸŸŸџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џŸŸŸџrrrџ‹‹‹џООО ГГГџЏЏЏџ™™™џ™™™џ™™™џ™™™џ˜˜˜џ•••џ”””џџ­­­џхххџѕѕѕџѓѓѓџѓѓѓџЮЮЮџ˜˜˜џАААџџџџшшшџ***џИИИџjjjˆˆˆЙЙЙџЛЛЛџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЖЖЖџЂЂЂџШШШzŠŠŠвПППџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ•••џТТТџіііџѓѓѓџїїїџЇЇЇџЎЎЎџLLLџџџ{{{џ>>>џ­­­џЅЅЅwЙЙЙGОООџЛЛЛџšššџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЈЈЈџРРРџПППШfefŠФФФџЅЅЅџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џНННџіііџѓѓѓџшшшџ”””џГГГџџџџЗЗЗџCCCџЙЙЙїЛЛЛLПППџПППџЋЋЋџ™™™џ˜˜˜џ™™™џ™™™џ™™™џ˜˜˜џšššџЏЏЏџРРРџПППЪППП#"#rЧЧЧџГГГџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џШШШџєєєџѕѕѕџЙЙЙџ­­­џKKKџџџ~~~џ&&&џЩЩЩџЈЈЈ.РРРПППдПППџСССџЖЖЖџЎЎЎџЋЋЋџЎЎЎџЕЕЕџРРРџПППџПППuПППqШШШџОООџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ•••џуууџѓѓѓџэээџ–––џЕЕЕџџџџšššџŽŽŽџЛЛЛŠОООООО6РРРЂПППнПППџПППџПППљПППМРРРq ШШШџСССџџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џџєєєџєєєџМММџВВВџJJJџџџ”””џ999џЛЛЛёППП<<<џФФФџПППџЈЈЈџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џФФФџєєєџяяяџšššџЂЂЂџџџ<<<џ///џЩЩЩџППП—‹Š‹џПППџПППџЌЌЌџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ–––џэээџєєєџМММџУУУџџџџ]]]џЙЙЙџЙЙЙLpKJKџЕЕЕџРРРџПППџЏЏЏџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џКККџєєєџхххџЇЇЇџ___џџџ\\\џŠŠŠџМММ%%#­MLMџdddџџЪЪЪџПППџЋЋЋџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ–––џ№№№џїїїџІІІџЎЎЎџџџ888џeeeџПППЊKт$$$џџџ'''р]€џ{z{џFFFџчччџџЩЩЩџРРРџЃЃЃџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џУУУџєєєџЩЩЩџПППџџџџGGGџУУУЯЪ+++џ џџџ€€€џ’’’џGGGџБ RД%$%џvuvџsssџfffџёёёџЩЩЩџ555џУУУџПППџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џœœœџјјјџяяяџІІІџfffџџџ$$$џЧЧЧлЈwvwџ{{{џ???џ___џ€€€џzzzџAAAџ999џmmmџд?Іџfffџ‰‰‰џaaaџiiiџнннџџџџџБББџџ™™™џРРРџІІІџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ–––џнннџјјјџЂЂЂџšššџџџџШШШшNRQRџХХХџБББџЇЇЇџ˜˜˜џœœœџЈЈЈџЂЂЂџЎЎЎџ џWWWџohїTTTџџXXXџJJJџšššџџџџџцццџzzzџ џџmmmџУУУџЎЎЎџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЙЙЙџїїїџЋЋЋџЦЦЦџџџ џШШШіЙУУУџœœœџ™™™џ™™™џ–––џђђђџіііџёёёџГГГџЏЏЏџ###џFFFјLъbbbџz{zџ444џoooџхххџЦЦЦџpppџџџџ<<<џ­­­џСССџЈЈЈџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џџљљљџНННџПППџ џџџЩЩЩћUUUэВВВџ™™™џ™™™џ™™™џžžžџїїїџѓѓѓџѓѓѓџїїїџЏЏЏџyyyџHHHџ\\\<ЊHGHџ€€€џBBBџwwwџ€€€џ000џџџџ444џrrrџПППџСССџЎЎЎџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ•••џъъъџФФФџЕЕЕџ???џџ џЩЩЩщrrrљІІІџ™™™џ™™™џ™™™џ———џїїїџѓѓѓџѓѓѓџѓѓѓџуууџБББџ111џ:::oфjijџ>>>џџ&&&џџџ###џ___џ™™™џШШШџЛЛЛџ­­­џЂЂЂџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џбббџХХХџ­­­џ___џџџЩЩЩжпІІІџ™™™џ™™™џ™™™џ˜˜˜џРРРџїїїџѓѓѓџѓѓѓџфффџ   џ@@@џ@@@qі‚‚‚џџџџџmmmџИИИџЦЦЦџБББџЁЁЁџ———џ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џКККџЧЧЧџЋЋЋџvvvџџџЩЩЩТƒ‚ƒ–ЏЏЏџ™™™џ™™™џ™™™џ™™™џ———џЏЏЏџвввџйййџЂЂЂџ   џMMMџeeeEьŠŠŠџџџџjjjџХХХџПППџЉЉЉџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џœœœџЏЏЏџЊЊЊџ€€€џџ555џЦЦЦЋ=8=ЕЕЕџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џ———џ™™™џЎЎЎџOOOќ٘˜˜џџџџЉЉЉџТТТџЏЏЏџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЊЊЊџŒŒŒџџNNNџУУУzЇЇЇБББџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џšššџРРРџЂЂЂOcjjjџCCCџџџРРРџПППџЄЄЄџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЎЎЎџ………џџlllџПППAНННСЙЙЙџžžžџ———џ˜˜˜џ˜˜˜џ———џЁЁЁџНННџППП…#"#)()џ˜˜˜џџџМММџПППџžžžџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џВВВџxxxџџ™™™џПППКККООО‡СССџНННџДДДџЕЕЕџЛЛЛџРРРѓООО]wœœœџџџЋЋЋџПППџ   џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЗЗЗџiiiџџФФФьЛЛЛ ППП_ПППŒПППŠОООW*)*я™™™џџaaaџСССџЊЊЊџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џУУУџBBBџџЬЬЬ’ЛЛЛЦЦЦ-wwwџKKKџџЦЦЦџЙЙЙџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џžžžџЩЩЩџџcccџТТТ4ППП mЕДЕџџЃЃЃџРРРџЃЃЃџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЈЈЈџЙЙЙџџЇЇЇіЋІІІџHHHџТТТџМММџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЖЖЖџnnnџџЯЯЯ•JJJОyyyџЛЛЛџПППџЏЏЏџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЩЩЩџџxxxџСССПППRQRЩЦЦЦџПППџРРРџІІІџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЉЉЉџ˜˜˜џџШШШБ\[\ХУУУџПППџРРРџ   џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џТТТџ---џkkkџУУУ*ППП```˜ФФФџПППџСССџžžžџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЄЄЄџЄЄЄџџЧЧЧО666pЦЦЦџПППџРРРџŸŸŸџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џТТТџ---џŒŒŒџНННПППКККџПППџРРРџІІІџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЊЊЊџ”””џ(((џЯЯЯy–––сПППџПППџЏЏЏџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ›››џШШШџџГГГхxxxoУУУџПППџОООџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џНННџPPPџ‚‚‚џТТТПППБББїПППџРРРџЄЄЄџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЏЏЏџџCCCџззз?ƒƒƒŽŽŽIСССџПППџМММџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЉЉЉџЊЊЊџ111џЮЮЮvАААЖРРРџПППџЎЎЎџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЉЉЉџЏЏЏџFFFџФФФ‚ПППЙЙЙиПППџРРРџЉЉЉџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џ­­­џРРРџ­­­џЯЯЯpМММwwwОООчПППџРРРџЋЋЋџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЗЗЗџРРРџПППџППП\ПППНННЛПППџРРРџДДДџšššџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џЇЇЇџОООџПППџПППтППП ППППППrПППџПППџРРРџЌЌЌџ˜˜˜џ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џЃЃЃџКККџРРРџПППџППППППППП ППП ПППџПППџПППџВВВџЃЃЃџ———џ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џ———џžžžџЌЌЌџНННџРРРџПППџПППЌППППППППППППППППППђПППџРРРџРРРџЙЙЙџАААџЉЉЉџІІІџЄЄЄџЁЁЁџ   џЄЄЄџЈЈЈџЋЋЋџДДДџПППџСССџПППџПППџПППЃПППППППППППП6ППППППЧПППўПППџПППџРРРџРРРџРРРџРРРџРРРџПППџПППџПППџПППщППП“ППП>ППППППППППППCПППPППП]ПППjПППwПППfПППBППППППџџџџџџџџџџџџјџџџџџџрџџџџŸџРџџџјџ€?џџ№џ€џџр€џџР?€џџ€?€џџ€€џџРџџРџРџр?џрџ№џ№џ€јџ€јџР?јџр?јџ№ќџјџќџўџјџџџџјџџџџ№џџџџ№џџџџР№џџРџќ€џр€џ€ўј№рР€€€€Рр?ќўџўџўџќџќџќџќџќџўџў?џў?џџџџџџџ€џџџ€џџџРџџџрџџџ№џџџќ?џџџўџџџџџ€џџџџџ№џџџџџџџџџџ(0`  202AAA-,-VEDEИXWXя.-.џ%%%џ,+,є>=>Ы\\\#"#? 'grRmxxxј“““џџЕЕЕџьььџеееџœœœџJKJџYXYіUUUПbab  ?+*+зBABџ555џ333џ???џJIJџ:9:­=<=!hghшШШШџџџAAAџџžžžџнннџїїїџЬЬЬџmmmџwww§=<=X$#$$#$’ONOџ*)*џџџ777џ\\\џfffџ“““џRRRѓYWY]@@@<•••џЦЦЦџ===џџџџџџ(((џyyyџбббџЗЗЗџiiiџSSSŽ;:;…™™™џcccџƒƒƒџЊЊЊџЂЂЂџЁЁЁџЁЁЁџ”””џ777џЌЌЌџPPPє~~~NMMMIЋЋЋџРРРџКККџІІІџ›››џšššџ›››џwwwџџџ џeeeџ№№№џ^^^џYYYО$popњМЛМџИИИџЇЇЇџ•••џЮЮЮџпппџЬЬЬџЎЎЎџЗЗЗџ000џƒƒƒџSRSбsrsyxy#ЇІЇџРРРџЇЇЇџЏЏЏџЭЭЭџПППџЈЈЈџЅЅЅџИИИџџџџ>>>џјјјџmmmџPOPŸ***323ЊЊЊџŸŸŸџ———џ˜˜˜џ–––џзззџљљљџїїїџєєєџЦЦЦџБББџNNNџUUUџtttX|{|ЈЈЈзМММџ›››џпппџіііџїїїџїїїџцццџЪЪЪџГГГџ–––џ777џџ666џыыыџrrrџiii‰ЇЇЇ\\\ьЗЗЗџšššџ™™™џ™™™џ™™™џœœœџДДДџчччџѕѕѕџѕѕѕџЕЕЕџЁЁЁџ999џUUUЧФФФЄЄЄ‹ЛЛЛџ˜˜˜џвввџєєєџѓѓѓџѓѓѓџѓѓѓџєєєџыыыџ­­­џЄЄЄџ666џџ333џпппџCCCџnzyzyyyџЏЏЏџ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џ”””џаааџіііџуууџ›››џNNNџ888џˆˆˆ?НННє   џГГГџ№№№џѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџіііџЎЎЎџ›››џџџgggџЏЏЏџTTTѕŽ2‰‰‰џЌЌЌџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џšššџнннџїїїџ›››џTTTџ)))џџџџЏЏЏЫГГГџ–––џЮЮЮџјјјџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџюююџБББџ–––џ џџЛЛЛџ€€€џsssНЦЦЦ‰ˆ‰яЏЏЏџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЎЎЎџѕѕѕџœœœџ```џDDDџjjjuuuFОООџџ™™™џбббџјјјџѓѓѓџѓѓѓџѓѓѓџѓѓѓџѓѓѓџнннџЃЃЃџ„„„џџџеееџnnnџˆ‡ˆdˆˆˆ‰‰‰šЕЕЕџšššџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ•••џШШШџ›››џWWWџoooдЦЦЦxxxŸŸŸуЋЋЋџ™™™џ™™™џЬЬЬџюююџђђђџєєєџєєєџѓѓѓџјјјџХХХџџ>>>џџ999џ“““џЁЁЁџgfgˆˆˆŒ,ИИИџŸŸŸџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ•••џ›››џhhhџ   `ˆˆˆ`ГГГџšššџ™™™џ–––џЌЌЌџОООџЧЧЧџбббџ№№№џѕѕѕџѕѕѕџЏЏЏџџ џџ~~~џlllџЗЗЗЁџџџa`a—˜—ЕЕЕ ИИИџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џАААџЄЄЄгККК†††ЊЊЊіžžžџ™™™џ™™™џ˜˜˜џ˜˜˜џ˜˜˜џšššџЇЇЇџуууџєєєџхххџЌЌЌџRRRџџHHHџIIIџЎЎЎэІІІ)ЗЗЗОООеЗЗЗџџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЊЊЊџССС№ТТТJSSS   ЪЌЌЌџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЃЃЃџшшшџѓѓѓџТТТџВВВџџџ’’’џPPPџШШШzЙЙЙЙЙЙРРРЈМММџЎЎЎџІІІџЂЂЂџЂЂЂџЇЇЇџДДДџРРРаПППIііі ОЛЛЛџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ–––џЈЈЈџёёёџчччџЊЊЊџlllџџ888џnnnџВВВЖЛЛЛ МММПППППП5РРРЄОООьНННџНННџОООРСССXПППХХХМММ ƒ‚ƒгТТТџ›››џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЕЕЕџњњњџПППџЃЃЃџџџˆˆˆџјХХХППП-,-9………џТТТџІІІџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џœœœџкккџѓѓѓџЁЁЁџQQQџџ333џƒƒƒџУУУBЊЊЊ IIIрИИИџРРРџЊЊЊџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џДДДџѓѓѓџЬЬЬџyyyџџџƒƒƒџЕЕЕ{AEDEфSSSџlllџТТТџЈЈЈџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џфффџщщщџЈЈЈџџџUUUџВВВЎ[ёџ$#$џ444ж  A ! !%+*+ЉPPPџeeeџНННџ```џТТТџ   џ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џЖЖЖџђђђџЩЩЩџCCCџџ$$$џЎЎЎз767žQPQџџ///џqqqџcccџAAAћw B)))‡RQRЪBABџwwwџНННџнннџ[[[џ–––џЖЖЖџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џюююџаааџ‰‰‰џџ џЋЋЋх'&'Y‚‚‚џЏЏЏџ˜˜˜џ–––џЂЂЂџ———џYYYџBBBџHi656рihiєNNNџvvvџшшшџЮЮЮџ___џ џiiiџОООџџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џЩЩЩџдддџМММџџџЉЉЉіCBCЪВВВџ˜˜˜џ˜˜˜џХХХџїїїџъъъџЙЙЙџEEEџGGGд$#$9'''мa`aџaaaџdddџИИИџ†††џ###џџ...џЂЂЂџМММџџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЋЋЋџпппџОООџџџЊЊЊњoooјІІІџ™™™џ™™™џЫЫЫџєєєџєєєџфффџ   џ<<<юGGG$€IIIњOOOџ666џ111џ%%%џ###џMMMџuuuџџЗЗЗџЇЇЇџ˜˜˜џ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЁЁЁџжжжџЗЗЗџBBBџџЉЉЉу‚‚‚щЂЂЂџ™™™џ˜˜˜џЎЎЎџђђђџѕѕѕџ№№№џАААџ===№4442ДdddџџџџzzzџЋЋЋџЈЈЈџ   џžžžџœœœџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џџЧЧЧџВВВџ___џџЈЈЈЬŽ‘ЅЅЅџ™™™џ™™™џ———џЇЇЇџЪЪЪџЗЗЗџЅЅЅџNNNшQQQ$#$ŒlllџџџiiiџХХХџГГГџšššџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЄЄЄџ­­­џlllџџЉЉЉЏŸžŸ ЋЋЋє™™™џ™™™џ™™™џ˜˜˜џ•••џ•••џГГГџeee‹%#%Ea`aџџџ———џКККџЁЁЁџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џЌЌЌџqqqџ%%%џЇЇЇuЈЊЈЛЛЛnЕЕЕџœœœџ———џ———џ˜˜˜џ­­­џОООА;:;ђMMMџ џЋЋЋџЕЕЕџ›››џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џВВВџdddџBBBџЉЉЉ7ГГГПППYСССчЙЙЙёЖЖЖѓМММщСССЌЌЌФФФr‰‰‰џџ–––џКККџ›››џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џИИИџQQQџWWWїЯЯЯПППОООНННТТТ5ТТТ>ТТТSRSлbbbџDDDџМММџžžžџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џФФФџ+++џjjjЙффф ОООПППППП)Ё Ёюџ­­­џЌЌЌџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џŸŸŸџМММџ џЇЇЇ{PŒŒŒїjjjџХХХџœœœџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЎЎЎџnnnџ222ђююю8KJKZ•••љРРРџЖЖЖџ›››џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џšššџВВВџџžžžРQPQ[ЖЖЖљПППџБББџšššџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џŸŸŸџ~~~џSSSџооо3ПППOOOAИИИѓПППџАААџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЇЇЇџOOOџЂЂЂДПППЗЗЗэПППџВВВџšššџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џ   џ™™™џXXXьЗЗЗœœœУСССџЗЗЗџ›››џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џТТТџ666џЅЅЅzПППsssHПППџРРРџŸŸŸџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЏЏЏџ[[[џ†††Уџџџ­­­ЕСССџГГГџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЄЄЄџƒƒƒџkkkдџџџПППvvvЋЋЋЛЛЛьРРРџЇЇЇџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЄЄЄџŸŸŸџnnnйЖЖЖПППЙЙЙQОООѕОООџЃЃЃџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џІІІџПППџАААлттт!ООО[ПППюНННџЈЈЈџ˜˜˜џ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џџЏЏЏџПППџПППЈШШШПППЗЗЗООО#ПППЯОООџЗЗЗџ   џ˜˜˜џ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џ™™™џЋЋЋџЛЛЛџПППџППП]ППППППППППППoПППъРРРљЖЖЖџІІІџџœœœџ›››џšššџšššџšššџšššџ›››џœœœџ   џ­­­џПППџРРР№ПППžПППППППППППППППXРРРšСССдПППњКККџЕЕЕџДДДџВВВџВВВџДДДџЗЗЗџМММџСССшССС­ПППtППП%ПППППППППППППППРРР6РРРbРРРoРРР|РРР‡РРРnРРРHРРР%ППППППџџџџџџџџџр?џџџџРџџР€џџ?€џў€џў€ќ€?ќ€?ќРќРќрќрў№ў№џ№џ€?№џрџ№џџџ№џџџрџџџРсџџ€џј€р?€><8€8Рpр№џрџрџрџрџрџрџрџрџ№џ№?џјџќџџўџџџџџџРџџџ№џџџџяџџ( @ 7('(v???vG:3<<<Фqqqџ^^^џšššџ………џFFFџZYZŠVUV<;<Љ0/0џџ^^^џfffџ@@@‡&%&lЪЪЪџџџHHHџvvvџжжжџПППџZZZё]]]+TTTфSTSџmmmџ„„„џmmmџjjjџƒƒƒџZYZЗxxx›УУУџœœœџyyyџjjjџJJJџџ!!!џЧЧЧџtttџLKLG323ˆНННџЌЌЌџ”””џчччџсссџТТТџџcccџNNNZ†…†sУУУџЂЂЂџпппџвввџЛЛЛџАААџTTTџџžžžџ}|}џ]]]<‚‚ёžžžџ™™™џ™™™џАААџчччџїїїџФФФџdddџ```иКККџЁЁЁџјјјџѓѓѓџєєєџ№№№џЗЗЗџyyyџџАААџbbbџЅЅЅzyzšššџ———џ™™™џ™™™џ™™™џ”””џбббџёёёџŸŸŸџџЏЏЏЛœœœџнннџѓѓѓџѓѓѓџѓѓѓџїїїџЖЖЖџaaaџџСССџ^^^фЂЂЂџ———џ™™™џ™™™џ™™™џ™™™џ———џёёёџЏЏЏџџjjj‡‡‡8ВВВџœœœџэээџѓѓѓџѓѓѓџѓѓѓџєєєџ­­­џ222џџ———џžžž‡ЊЊЊИ   џ™™™џ™™™џ™™™џ™™™џ™™™џЇЇЇџЌЌЌџ:::нЌЌЌу˜˜˜џœœœџжжжџєєєџјјјџѓѓѓџцццџŸŸŸџџrrrџtttџ———ˆˆˆa`aœœœ+ЗЗЗџ———џ™™™џ™™™џ™™™џ™™™џ———џ™™™џЇЇЇ_˜˜˜vЉЉЉџ™™™џ———џ”””џ›››џЭЭЭџјјјџПППџmmmџџvvvџЙЙЙЁНННcЖЖЖџœœœџ———џ˜˜˜џ———џЌЌЌџТТТЈ2225КККџ˜˜˜џ™™™џ™™™џ™™™џ———џФФФџїїїџЏЏЏџ џNNNџzzzџППП2ТТТПКККџЗЗЗџОООеРРРYМММ:ЦЦЦџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ•••џнннџЭЭЭџmmmџџџШШШWПППHGHЉЦЦЦџ   џ™™™џ™™™џ™™™џ™™™џ™™™џŸŸŸџљљљџГГГџџ---џХХХ)()‡TTTџЦЦЦџЂЂЂџ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЮЮЮџЭЭЭџ>>>џ џ˜˜˜Ъ… џ&&&џOOOл  kGGGъ†††џvvvџЫЫЫџšššџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЁЁЁџюююџ~~~џџzzzъ&%&|ЄЄЄџyyyџ’’’џrrrџ555џ  PNMNСFFFџЎЎЎџЕЕЕџmmmџmmmџЌЌЌџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ–––џчччџЏЏЏџџkkkјƒƒƒы———џ–––џћћћџчччџ~~~џIIIŽHHHкkjkџZZZџcccџ"""џ<<<џžžžџЉЉЉџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џШШШџСССџџgggљ‘‘‘ј˜˜˜џ–––џэээџіііџЬЬЬџ444Т ?HHHџ  џџcccџšššџГГГџЂЂЂџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЏЏЏџНННџ///џhhhцЇЇЇЏ———џ™™™џ›››џЖЖЖџЃЃЃџLLL“'WWWџџxxxџЛЛЛџ›››џ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЉЉЉџ:::џwwwЪЄЄЄБББџ˜˜˜џ———џ™™™џЖЖЖєДДД_^_цџŸŸŸџЇЇЇџ˜˜˜џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЈЈЈџ333џ”””—ЙЙЙРРРСССЄЙЙЙЯРРР—МММ ЂЂЂRUUUџuuuџЉЉЉџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џАААџ&&&џмммXОООППП{z{Џ777џРРРџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џЛЛЛџ===џgggуЄЄЄџІІІџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џ„„„џjjjГ•”•ьСССџџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џЌЌЌџCCCџєєє-ПППšššЪСССџ›››џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ———џџnnnНТТТџ   џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џАААџWWWџџџџППП888НННџЏЏЏџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џžžžџsssџЈЈЈPГГГ€РРРџœœœџ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ›››џџ~~~~НННЗМММџ›››џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џžžžџОООџНННrЗЗЗООО‹ОООџЇЇЇџ———џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ™™™џ˜˜˜џšššџЎЎЎџРРРѓППП6ППППППРРРГНННџЏЏЏџІІІџŸŸŸџœœœџœœœџ   џЈЈЈџДДДџСССєПППpПППППППППNРРР„СССЈСССГСССЙСССœРРРzППП,џџџџџџ€џќ€ј?№€№€№€№Р№РјрќрўрџџРџџ€‡џƒјрР€ƒЧўўўўџџџџ€?џРџ№џџўџ(  `b`jij)*)&WVWБ|||Г?>?^XXXџџџGFG1HHHњSSSџggg№WVWЄЄЄЯ:::џUUUџkkkџ‹‹‹гYYYъџтттџХХХџ\[\иАААЊЫЫЫџхххџАААџ,,,џŠŠŠоdcd›››џ™™™џ”””џзззџZZZџЇЇЇ4ЏЏЏџїїїџїїїџЖЖЖџ444џ†††žЌЌЌХ———џ™™™џšššџ‡‡‡лЄЄЄоЅЅЅџЦЦЦџяяяџnnnџVVVџЩЩЩ#ІІІЪЪЪ ВВВЩЈЈЈџЎЎЎзддд’’’ЁЁЁ ———џ™™™џџрррџ"""џ‘‘‘š›››ѕœœœџ™™™џ™™™џСССџlllџ^^^кbbb}IIIџIIIШ323ь”џ<щ’џ9цŽџ5у‹џ2пˆџ/м„џ,иџР€DџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџР€DџšNџ#Хsџ>ь”џ>ь”џ;ш‘џ8хџ4тŠџ1о‡џР€DџЯŸrџЯŸrџЯŸrџЯŸrџЯŸrџЯŸrџЯŸrџЯŸrџЯŸrџаЂvџгЈџжЎˆџйД‘џмК™џпРЂџтЦЋџхЬДџцЮИџР€DџšOџЏ_џ%Шuџ.дџ-д€џ+б}џ)ЯzџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€Dџ›PџQџšNџšNџšNџšNџšNџšNџšNџšNџ›NџœPџ€МџНџОџ€Нџ€Йџ~Тџ~Тџ3“аџG иџMЄлџ;˜гџ„Цџ~ТџТџ†Чџ^ЎтџnЙщџnЙщџnЙщџnЙщџjЗчџ7—вџ}Сџ~Сџ€УџkЕхџnЙщџnЙщџnЙщџnЙщџnЙщџnЙщџnЙщџ.ЭџТџТџYЈкџ„УьџwНъџnЙщџnЙщџnЙщџnЙщџnЙщџnЙщџfДцџ~Тџ}СџƒРчџ’Ъюџ„УьџwНъџnЙщџnЙщџnЙщџnЙщџnЙщџnЙщџ ‡Чџ}Тџ}СџЅг№џŸа№џ‘Ъюџ„УьџwНъџnЙщџnЙщџnЙщџnЙщџnЙщџ3“Яџ}Сџ}СџЦщџЌзђџŸа№џ‘Ъюџ„УьџvНъџnЙщџnЙщџnЙщџnЙщџ#‰Щџ~ТџТџjБнџ­зђџЌзђџŸа№џ‘Ъюџ„УьџvНъџnЙщџnЙщџhЕцџ}СџТџУџœЭэџ­зђџЌзђџžа№џ‘Ъюџ„УьџvНъџnЙщџ4”аџ~Тџ€Тџ.Ъџ•Щыџ­зђџЌзђџžа№џ‘ЪюџТыџEžжџ}ТџУџ€ТџOЁдџpДпџ{КтџWІиџ%‰Шџ~ТџУџТџ~Сџ€Уџ}Тџџ№џ№џ№џ№рррррррррр№јќ?џџСџџџџџўџќ?џќ?џќџќџќџќ?џќ?џўџџџџџСџџџџџџџџџџ(  Ы›nџжЗ™џжЗ™џжЗ™џжЗ™џжЗ™џжЗ™џжЗ™џжЗ™џЫ›nџжЗ™џьююџьююџьююџьююџьююџьююџьююџьююџжЗ™џ›N€ ЅVџ ІWџ ІWџ ІWџжЗ™џьююџшьыџWЗ†џощфџьююџьююџьююџьююџжЗ™џ›N€ПkџТkџНgџНgџжЗ™џьююџhО’џSЖƒџsС™џьююџьююџьююџьююџжЗ™џœO€!Щtџ"ЭvџЦpџРjџжЗ™џьююџŽЫЌџцыщџЂгКџЕкШџьююџьююџьююџжЗ™џ›O€*в}џ-й‚џ&в{џ ЫtџжЗ™џьююџьююџьююџьююџŠЪЊџьююџьююџьююџжЗ™џœO€*а|џ7фџ1о†џ+з€џжЗ™џьююџьююџьююџьююџьююџьююџьююџьююџжЗ™џЈZП-гџ3н‡џ.и‚џУ‡OџЧ[џЧ[џЧ[џЧ[џШ’_џЫ˜hџЮžpџбЄyџЩ“aџœP€šN€ Œ…џ Œ…џ ‘qП›O€Т@(ŒЫџUЉоџYЋрџ4”аџ}С@%‰ШџuЛщџnЙщџnЙщџnЙщџ.ЭџRЃжџ‘ЩюџxНъџnЙщџnЙщџKЃкџ}С€GœвџЉеёџ‘ЩюџwНъџnЙщџBжџ~Т@€ТПƒОхџЈеёџ‘ЩюџjЕхџ…ЦПТ€9”Юџ>—ЯџФПќЌAќЌA€ЌA€ЌA€ЌA€ЌA€ЌAРЌAрЌAрЌAрЌAр?ЌAр?ЌAрЌA№џЌAџџЌAtortoisehg-4.5.2/icons/menublame.ico0000644000175000017500000000344613150123225020300 0ustar sborhosborho00000000000000 h&  ˜Ž(  @lllџ>џ>џ>џџџџT|џEEEџ|Tџ›iџ|Tџ|Tџ]?џ>>џџRRRџT|џi›џ?]џ|TџК~џК~џ›iџ›iџ›iџ>*џRRRџT|џ~КџlЮџџ?]џEEEџHџ…џHџ…џК~џК~џК~џ]?џRRRџT|џ~КџlЮџџT|џ’’’џК~џ$џmџјјјџоооџК~џ]?џEEEџT|џ~КџкџџT|џ………џ€€€џlllџ999џџ999џ___џ*>џ“йџкџџT|џ___џ’’’џlџџlЮџџџцДџџкџџкџ’’’џџT|џ?]џlllџlџџlЮџџкџџ~КџјјјџџџџџџцДџ’’’џ___џ€€€џкџџкџџДџџџlџџ___џЄ  џџџџџџкџ999џ’’’џкџџДџџџџџџыыыџыыыџџцДџјјјџџкџџ’’’џlЮџџlџџРРРџ___џРРРџxxxџџкџ999џlllџџцДџјјјџџџџџјјјџџцДџџцДџџкџlllџxxxџџцДџјјјџыыыџџцДџџцДџxxxџlllџ999џџ999џlllџќ№рСƒР€€€€РРр?№џџџџ(  p‚Xџ’bџVџgFџ;8џ*JGВQŽJpt”Uoџkœџ ƒZџ ФџЎsџŸlџ_џ&eXБRz{’)Ujџ wЋџLБуџPu_џ:ѕџ;оџ4У•џЇqџ*vcБV~||2Teџ sЃџ?Ѕзџ'p‘фsž’ы0Хљ™е­џПППџ\џ'[NзZ|y›5LTџqЃџBЃвџ=ƒЁтa†#n‘0zІчŠЁџnЇџ~s^џpaEџzthџ 5>џЗџGŠЇцXƒ‹>l•Œeoy‹џtЈчџyЭўџ ПИџ§щСџлЬЏџjhcџ.]qёO|„Rsœ“ez“­џЫџџœщџџO“гџЕЕЕџыуеџжФ џT]\йyЄ›KЊЙџчўџŸќўџЙШцџФНБџючлџўрЅџ,44и~ЎЃ‰ЂЁЪˆЧрџyСўџПППџЇЄžџДБЊџыЮ“џ4њ64.`џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ­ЮЫPыќџxяќџЦјўџЌѕ§џZшњџOщќџOщќџOщќџ@МЬџ˜Вшџџџ64.v6O<ї4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ6D7ї64.`џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ­ЮЫ8въџ.Нйџ>Ойџ8Ойџ3Рмџ+Мйџ&Лиџ"Кзџ(ГЬџ˜Вщџџџ64.>64.џ4Эџ4тŠџ4тŠџ4тŠџ4тŠџ6\Cњ67/œџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЊЫуJЫф§‡ю§џ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџŠ№џџhм№ўЉЪћџџџџџџ690б6T?њ4й…џ4тŠџ4тŠџ4тŠџ5Јkџ67/љ64.'џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЋЬs6Окэxхїџ‰яўџ‹№џџ‹№џџ‹№џџŠ№џџ}шљџSашѓЋЭМџџџџџџ64.680у6_Eћ4н‡џ4тŠџ4тŠџ4р‰џ5jKћ680№64.4џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЃТ2ЌЮ–­ЮНБаЮ!ВвлБадЎЮЦ­ЭЄ ЄЧWџџџџџџџџџџџ64.#680ь5kKќ4с‰џ4тŠџ4тŠџ4р‰џ5uPќ66/ї64.5џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ64..670є5{T§4тŠџ4тŠџ4тŠџ4сŠџ5sOќ670№64.&џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ64.›67/Ћ67/Ћ67/Ћ67/Ћ67/Ћ64.mџџџџџџ64.<67/ј5‡Zў4тŠџ4тŠџ4тŠџ4п‰џ5dGњ680с64.џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ6;1и6WAџ5zSџ5zSџ5zSџ5bFџ6:1цџџџџџџџџџ64.D64.џ4Оwџ4тŠџ4тŠџ4тŠџ4и…џ680ќ64.Uџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ670Џ5ySџ4тŠџ4тŠџ4тŠџ4Хzџ64.џ64.#џџџџџџџџџ691Т5uPў4тŠџ4тŠџ4тŠџ4тŠџ5cGљ64.Œџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ64.ˆ6]Dј4тŠџ4тŠџ4тŠџ4тŠџ6E7њ67/Ÿџџџџџџџџџ6:1и5Š[џ4тŠџ4тŠџ4тŠџ4тŠџ5yRџ66/Јџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ64.d6B6ј4тŠџ4тŠџ4тŠџ4тŠџ4Мvџ680ќ6:1е64.˜6;2б64.џ4Тyџ4тŠџ4тŠџ4тŠџ4тŠџ5dGљ64.–џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ64.:64.џ4Хzџ4тŠџ4тŠџ4тŠџ4тŠџ4Х{џ5‹\џ5gIћ5Ž^џ4Кuџ4тŠџ4тŠџ4тŠџ4тŠџ4п‰џ6:1ћ64.Vџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ680М6\Cњ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ5Іjџ64.џ64.џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ64."66/њ5 gџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4Кuџ680ќ65.~џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ65.y65.ў5tPќ4Ю€џ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4тŠџ4к†џ5_џ6;2њ66/џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ64.?691а670ќ5nM§5‡Zџ5žfџ5Еrџ5Жsџ5šdџ5}Uџ6>4њ680я64.t64.џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ64.64.W65.Ё6:1Ч6:1ч65.§65.§6:1ф691Л64.r64. џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ64.64.џџџџџџџџџџџџџџџџџџџџџџџџџџџџ€?џџ€?џџ€№?€`€`€`€`€p€|џ€џќџќ@?ќ@?ќ`?ќ`ќ`ўpџјџџќџјџјџјџјџќџќџќџўџџџџ€џџр?џџџџ(  <Јџ; Мџ;ŸМџ+ŒЖџ0˜аџMнѕџMнѕџTпѕџ Бшџ|Ъџ?Ідџ}ьљџ}ьљџ„эњџCађџ)•жџ;ІдџыљџыљџэљџAађџ)”еџ;Ѕгџьљџьљџ‚эњџAађџ)“еџ6Ÿаџpшјџpшјџtшјџ7Цяџ'гџ0Љйџ[пјџ[пјџtшјџ7Цяџ#ŠЮџp[Nџp[Nџp[Nџp[Nџp[Nџp[Nџp[NџLИкџLИкџLИкџLИкџcahџџюФџўјэџўјэџўјэџўјэџcahџ|Tџ|Tџ|Tџ|TџKnuџџюФџџіуџџіуџџіуџџіуџKnuџBvџџюФџџюФџџюФџџюФџџюФџBvџ|Tџ|Tџ|Tџ|Tџc2џg5џj8џv@%џ{D)џ~H-џ‚K/џЎGџо’IџзЅZџэМ‰џљЮ’џяуЗџ‚K/џc2џg5џj8џu@%џ{D)џ~H-џ‚K/џџУџџџџџџУџџџџџџџџ(  SБЬGЊШЊ>Жџ>ЈФџ1šРџFДиZ\ЌФ?BЇвџZсіџ]тіџ@Ъяџ"ŽгџfЎО?MЏгџ}ыјџьљџ[лєџ. лўhІВ?L­аџыљџ€ьљџZлѕџ-ŸкџjЂЌ?GЉЭџtщјџuщјџRеѓџ+šйџ}gWП{fWПxhYПvj\Пuk_Пz’Œ/w  EЉЫы^рјџfујџOгђџ'–еўr_Tџ„qaџ„reџ„reџm`џv}uБ~m2Ф}r:ШRЉЙџRЎФ№PЖгкY­И+|€}џўёгџўїъџўїъџнкаџm‚‚В€UЖo4к}p6кx‡cmkŠŒџўюЪџўёдџўёдџкиФџf‡‰Б„Žqpƒˆc‘‰e‘{—AeYOџЈ…cџЎˆgџГŒkџЅ…iџx{qВˆpA‡„Xm„…[m”xAŸKџК}@џУ‘^џгЃrџПЁ|џˆoXЅo7ўxD"ўM-ў‹V6ў‹Z=ўˆkR˜ўўўўўpp№№№tortoisehg-4.5.2/icons/menucommit.ico0000644000175000017500000001373613150123225020513 0ustar sborhosborho00000000000000  Ј6 hо  ˜F( @ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ›NŒ •Jџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ›P@šNџ™O-џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџœOВšNџ˜L%џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ ›N›Oљ RіšM]џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ™On ЄVєА^џœPПџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџœOЕА^џПjџžQіšNXџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџœPъКfџХoџПjџ›Pі˜L/џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ›OѓПjџШrџ ЬuџЛhўPщ”Qџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ •Jџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ›OѕРlџЫtџ#Юxџ&в{џГbљPШŸ@џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџN šNЭџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџPлМhџ!Эvџ%бzџ)е~џ+з€џ­^їžQк˜L%џџџџџџџџџџџџџџџџџџџџџџџџЊUOХšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџœP†­^њ#Юwџ'г|џ+з€џ.л„џ2п‡џТo§œPѕ›PYџџџџџџџџџџџџџџџџџџ€€œOБЁTїšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџœP6žQі#Эvџ(д}џ,иџ0н†џ4сŠџ8хџ.жџ ЁSіQžЂF џџџџџџџџџ™fœPЊЁSєЙdџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџœPВА_љ(д}џ-й‚џ1о†џ5т‹џ9чџ=ы“џ5сŠџА`ѕPя™Og€@™M œOО ЃUѕПiџЛeџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ —Q›PѓХqџ,йџ0н†џ4тŠџ8цŽџ:шџ8хџ4сŠџ&Эxџ ЅWѓœOсœPг ЈXіФnџСkџЛeџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџšOQPі"Шtџ/л„џ2пˆџ5т‹џ6уŒџ5тŠџ2п‡џ.л„џ+з€џПlџВaћШsџХnџРjџЛeџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ™NKœPѕ"Чsџ/м…џ1о‡џ2пˆџ1о‡џ/м„џ,иџ)е~џ%бzџ!ЬvџШrџФmџПiџЛeџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ™M<œPѓКhќ-й‚џ.л„џ.кƒџ,иџ)е~џ&в{џ#ЮxџЪtџЦpџТlџОhџЛeџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџœN$PШ ЃVє"Ъtџ*жџ(д}џ&в{џ$Яxџ!ЬuџШrџФnџРjџНgџЛeџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ›McœPѓБ`љ#Эxџ#Юxџ!ЬuџЩrџХoџТlџОhџНgџЛeџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ™M(›PЉšNџЋ\џЪsџШrџЦoџТlџПiџНgџНgџЛeџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ’I›MYPЮ›OћЌ[ћОkџХpџФnџТlџПjџНgџЛeџЛeџЛeџЙdџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџOQšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџšNџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџўџџўџџў?џџќ?џџќџџќџџќџПќџ?ќў?ў|?ў8?џ?џ€?џР?џр?џ№?џќ?џќ?џ№?џр?џџџџџџџџџџџџџџџџџџџџџџџџ(  |(џ|(џК<џ|(џ›2џ|(џ|(џК<џ|(џ|(џК<џHџ…џ|(џ|(џ|(џ›2џHџ…џ$џmџ|(џ|(џ|(џ|(џHџ…џHџ…џК<џ|(џ|(џ|(џ›2џ|(џ|(џ$џmџ$џmџК<џК<џК<џ›2џ|(џ|(џК<џ$џmџК<џК<џ›2џ|(џ›2џК<џК<џ›2џ›2џ|(џ|(џ|(џ|(џ|(џ|(џ|(џ|(џ|(џџџџџїџчџуџуџсїрч№јќўјџџџџџџ(  |(џ|(џК<џ’4_|(џ›2џ|(џ|(џК<џ|(џ0|(џК<џHџ…џ|(џ™5“2_|(џ|(џ›2џHџ…џ$џmџ|(џŠ.|(џ|(џ|(џHџ…џHџ…џК<џ|(џ|(џ|(џ›2џ|(џ|(џ$џmџ$џmџК<џК<џК<џ›2џ|(џ|(џК<џ$џmџК<џК<џ›2џ|(џœ7/…,П›2џК<џК<џ›2џ›2џ|(џ|(џ|(џ|(џ|(џ|(џ|(џ|(џ|(џп№Ÿ№№№‡аƒРр№№рџ№tortoisehg-4.5.2/icons/TortoiseMerge.ico0000644000175000017500000005372613150123225021131 0ustar sborhosborho00000000000000hf ЈЮ00Јv h  Ј†!00 Ј%.2( >:7B=;CBBNF@TKEWMG|_Rzrm{zyŒ/вF„i]ŸiUОnS‡qcŒwj˜|j€}{~xЃ}jЂ~mЙwaГyfД{hтoHўvJѓ|U§Uн`™„v’„|І†r „tЊsЖ‘x§€Wѕ[Ь€gн€`Я‰rИ~Ц›р†gс‹nј‡bћ‹eѓlњkў“oтŽsќ“p§•r§˜vђ™|§™xћ~§ž~ƒ€‰ˆˆ——–Е•ДЂ›Ъ˜ˆв™…б›‰вŒўŸ€ХЁ•ФЈŸфЄŽў €§Ѓ„ўЄ†§ЇŠсЄсЎ§Ќ‘ўЕФЉЁЦБЊФДЎбКВбНЕсЙЋсЛЎ§З ўЙЂўМІўНЉсНАвРКвСМўТЏсФК§ФБўЦДўЩЗўЪЙўЮНпЮЧтЫФсЭЦўаСћгФўдЦўжЩўиЫўкЮўмбўпеўткџхншьэћъфўьцћёэіѕђ< KBLYN?:# Ta]XF"))* ^kg`YXNO ;SlgaagaHE'-%Skgqa`1E-&SusgX9$.E>Qtk]M96 SmZQlaN0 /5(\omlTOs`9991(doommTDHJH955dooml=BMH99'fomSBJ9,f\@2џOѓрР€€РррР€€Рс‡ѓЯџџ( @- #!))),,,333410642444876?95:::<:8>=<F* S0%F>9FCBTFAQJE]RJ[TOUUUnPF`ZVf\Uh_Yl`Yld^saVqaXyi_kfcrhcth`wleuokunlspovpmvvvxtryuzzz|z~~~ž1  'Ѕ)х9L:‹P=ŒP<М^>иS'ХY6рR$ўX!рd:љc1ћj9ќn?†VFŒTBƒZLŒYH‹\MfY‚h_›fRœeRОcDАeKПfI lZ€kd‰mala–n`‚pdpdtktlˆxm{o‘ve™qd•yf™th–|k—}m›{jœ~mwpxp}rztŠ~w|x†~z~|ЄscТbBгhEТmQЯoPУpUЦx_ѓnBњoBцpH§qBёtJўvIђxOўzNэxQђzRє~Wў|QѕX€{›ƒu™‚z™†ЉŠvЌ‹uЇ…zЋyЎyГwДwГyЕyМ”{И“|Й•~ў€Vѕ[іƒ]і„_џXў†^ЦƒlЦ†qЦˆsЦŠvЦ›ї‡b§‡`ўˆaј‰eў‹eљŒgјŒiџkќlџkљmќ‘mќ’p§”q§–t§˜vўšyўœ{ўž~ƒƒƒ‡‡†‹‡„‹‰ˆŽŽŽ™‰ƒ™‘’’’”””š—–˜˜˜œ›šЊІЄЋЋЋПЗДПИЖПКИПНМЦ•…Ч›€Чœ€Шœ€ўŸ€ЦЃ˜ЦЄšХЈŸюЃŠўЁ‚ўЂ„џЄ…ўІ‰ўЉьЇќЋџЌ‘џЏ•џА–ўВ™ўД›ўЕЦЊЁХ­ЅХЏЈЦБЊЦЗВЦОМўИЁ§КЄўМІўНЉўРЌўУАўФБўЦДўШЖўЪИўЬКўЮНХХХўаСўгФўдЦўжЩўиЫўйЭўлаўмбўодэсмўржѓтйўтйџфлџхнъчшљшсџшрџьхѓющџющїѓяџ№ыы№ѓсњџьўџўєёўљї§ћњБЎБc--,ЈД#…$-$))CЭ‘ЗaT) UUЎ{ззЫ‘*ˆT  MйизЭЭЗ-YˆWY"Љ{окйзеЭЪe- …“““““U ˆ,ЊRтоккйззЫK'$\Л“““““ƒˆK",ЌтттоккизЭЧСFЛ““““ˆDza aщцтттокизеЭЭзсBF[~ˆƒ~MHpf"Г,ЎщщщцттокйизикШкс2kNMPJitrn>АЈЋащщццттокитцзݘЭсBžž”ŒŒvvrn@сЗащццттонт№ок§кwис4ž˜”Œxvv5ЕацццтоыљцкзцХ=ЪсBžš”ŒŒ5ЗатттэњёткзЪЄ˜q<зс3ž˜”IЗПтђћѕщтоиЭФЁwq;ХсBžhЗдўїјјцокзШЄžЭmзсЗЗдђўўцткиЪФžкќ”.ЗацRгђђщцойЭШЁžЃ6/7ЁjЗащццRг№щцокзЪЄž80gЄЄЁjЗвьщщцт]в№цтнзЭФl1:ЄЄЄЃžjЗв№ьщщщццRбщщўцЭ–9ФФЄЄЄЁžhЗг№№№ьщщццт]а№ќщШsШФЄФЄЃЁЁžhѓђ№№№ьщщщцтRатЭžЄШФФФЄЄЄЁЁžžyђ№№№ьщщчццтRЙФФЪШШФЄФЄЃЃЁЁ4­ђѓ№№№щщщццтЪШШФФНЄЄЄЃЁVyѓѓ№№№щщчцПЗЗ‘ЪШФФФЄЄЄЃ?­ѓ№№щ№щщПШШФНФЄЄXy№№№ьщаЗЗШФФЄФ?­№№№вФФНUy№вЗЗФ?џџ0џџО0џўќј№рР€РР№№ќ?ќ?ќ?ќ?№№РР€Рр№јРќР?ў№џ№џџП§џџџџџ(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~~~ž'­+х9ŸU=КZ:Н_?џVљd3њh7ўn>†RA’^LИ_Bˆf[ЌcKЁeQЏp\nb‡reˆqcŒvhˆyo”xf“sh™wl’zkš}jŒ{p|pv~y“~q”~qХdCРgJЧlN№qG№tJђwNќvJыyTѓzRѕ~Vџ|Q‚€•‚v™ƒvœ„užŠ~Ё‡vЇ‰w­‹vЅŒ{ЎŽzГqЖ†vЏ|БzЗ‘yЕ’}Ж”~ѕ[і„^џ„[Р—~Т™Ч›цƒcїˆcјŠfџŒeї‹hѓŽkљjџkњ‘nќlџ“pќ”qџ—u§˜uў›z§œ{ўž~€€€‡‡‡ŒŒ’’’–––˜˜˜œœœЌ‹€Џ“Д—„Н™‚О›„ЁЁЁЅЅЅЌЌЌВВВТšФ›ТœƒХœФ„Шœ€ЪŸ„ўŸ€ЫЂˆ§ €ўЃ„џЇ‰џЊŽџЎ“џВ™џЖžўКЃџНЈўС­џХВўЩЗџЭМЬЬЬвввоннўбСџдЦўиЬџмбўржџфллєџџшрџыхџѓях§џђџџћџџџџџžœœBBœEEBŸBgbEЁЁЁEžž1œЊУž6ЎЎ@E7,EEЛЛЙEx]  6yББ|9@ ЊММЛКЙ ZЎЎ]6  ^ОНММЛКЛ'„„'   ООННМЛКЙЙG vБa)%\ BЉППООНММЛКЙЙE 1ІЎБББЎЎБ‚Ї‰,œССППООНМЛЛЛЙ~7;@ €ЎББББ‰БББЎ €ЎЎ@DBœХСРРППООММЛКЙ6BЉ5БББББББББББ/2БББa6ЁХХХССРПООНММЛК€~ИS9ББ‰ББ‰ББ‰БЅ#DЎЄБД„#ХЧХХХСРПППОНМЛКЙЙИЙRdЎББББ‰БББu(*V;y{@>>#ШШЧЧХХХСРПООНММЛКЙМЛв“Y‰БББББЎv3TmlK@;ФžЋžШШШЧЧЧХХССРПОНММЛКПНМвв“W&1\gg\'$jmnmi 7Ÿ#ЁЩШШШШЧХЧХСРПООНМЛСПО•sвв“‹X_UU`kprnnnmLТЈЈЩЩШШШЧЧХХССРПОННШЧСГ“‡oвв“‘‘Œ††††rrnnmLf7ЁЩШШШЧЧХЧХСРППОЪЩЧЙРвЛsoвв“‘‘Œ††r†rnrnmШШШЧЧХЧСССРПЭЪЪМЛввв‡sQвв““Œ††††rnrШШЧЧХЧХХСПЫЭЭПМЛХвО‡sQPвв““‡†r†rШЧХЧХХССЯЫЫСРОМЛЗ™“‡oQPвв““‘†††ЧЧХЧХСЯЯЫЧХСПМЛИЗ“‡sQPOвв““‘Œ†ХЧХСаЯЯЩШЧССОМЙЗ™“‡oQOOвв““‘ХХвааЪЪШЧХРПНЛИЗ“‡sQPNOвв““вваЪЮвЮЧХСПНЛЙЗ—‡ЙвЗOOвввЭЪвввШЧСРОМЛИ™“‡вввNNЧХЬЭЮвЮШЧХРПМЛЙЗ•ЛвЗOI—“ШЧЧХЬЬЪЪЩЧХСПОМКЗ™“‡oQIII™—•“ЩШШЧЧЧЪЭЪЩШЧССНМЛИ›—sHHH™™™—•“ЩЩШШЧЧХЧЪЪЩШЧХРПНЛЙИ—’JJH™З—™—••“ЪЩЩЩШШЧЧХЧЪЪЩЧХСРОМКЗЗJJJЗ™™™™™••““ЪЪЩЩЩШШШШЧХЧЪЩШЧЭвЩМКИNNNЗЗ™З™™™™••“‘ЭЪЪЪЪЩЩШШЧЧЧХХЩШЧвввНЛsQNЗЗЗ™™™™™•••““‘ЪЭЪЭЪЪЩЩЩШШЧЧЧХХЩЧЬвЩОssИЗЗ››З—™™™•••““‘ЭЪЭЪЪЪЪЩЩШШШШЧЧХХШЧХР™“‡ИИЗЗЗ›™›™™——••“““ЬЬЬЭЪЪЪЪЩЩШШЧЧЧЧХХЧХИ™“ЙИИЗЗЗ™З™™™——˜••““ЬЬЪЭЪЪЪЩЩЩШШЧЧЧХХХЛЗ™ЛЙИИЗЗ››™›™™—˜–••“ЬЭЪЬЪЪЪЩЩЩШШШЧЧЧХХИКЙЙИИИЗЗ™›™™™™—–•–ЪЭЬЬЭЪЪЩЩЩШШЧЧХХХКЙЙИИЗЗ›З››™™———–ЬЬЪЭЪЪЪЩЩШШШЧЧХЙЙИИЗЗЗ™››™››˜˜ЭЪЬЪЪЪЩЩЩШШЧЧЙЙИИЗЗЗ›››™›™ЭЪЭЪЪЪЩЩЩШШЙИИЗ›З™››™™ЭЪЭЪЪЪЩЩШИИЗЗ›››››ЭЪЪЪЪЪЩИЗЗЗ››ЗЭЪЪЪЪЗ›З››ЬЪЪЗЗ›џџџўџџџў џџјќџ№|?џр<џРџ€?џўќј№рРРРр№јќ?ўџџџ€џџРџџРџџ€џџџўќ?ј№рРРРр№јќ€?ўРџрџџ€№џџРјџџр?ќџџ№ўџџјџџџџџџџџџџџџџџџ(  @8888ВБАKrrrЪwwwKzlcкxwvљusrŒ   gЦПНTсЃŽџЦ‘€тттт•ŒŠvjў555ыNE@ўTLFў>;8ўea^ЏЦРОTтЎџўНЈџўДœџН‹zтb_]ЄC?=ў˜|iўЗwўЇ‡rўXMFўŒwiшxvtyЭЭЭЦСПTсЙЌџўЪИџўТЏџўЛЄџфЄџrdьuWKѓР—~ўЧ›ўЧ›ўˆpbўƒi]ўЁ…sўsic№8тУКџўзЪџўаСџўЧЕџўНЉџўИЁџўЗ џХЋЂџŸiUўЁ~lўЃ}kўНmR§уoIџ~xў€€€я888ХАЉтўкЯџўдЦџўЬЛџўЧЕџўжЩџўЪЙџўŸ€џХЉ џн~_џї‡bџѕZџѓ{SџЌiRтЦЦЦ5тттХ­ЅтўдЦџћгХџшьэџўЫКџўОЉџў’nџќ~TџФЈŸџн`џїˆcџПrXтттттттХ­ЅтѕіѓџћъфџўаСџўЛЅџџŸџўXџћ‹fџФЈŸџЇkWтттттттЉ”тЦДЎџџѓяџўжЩџўУАџўЌџџŸџє™{џŒ/џЈn[тттттттХВЋтўлаџсНВџЦЏЈџўмбџўЪИџўЕџќkџвFџєnџ§—uџФ}eтттт888ЦЕАтўукџўогџўйЮџсНБџХЋЃџўьчџўФБџўwJџџœzџџž}џўšxџќ“oџУ{cт8888тЮЧџџхнџўтиџўмвџўиЬџсЛЎџХЁ•џўЃ…џџІ‰џџЅ‡џџŸџ§šxџќ•rџр€aџ8ЦУТTтЮЧџџцнџўрзџўмвџўзЫџЉ‡тЦ~тџЋџџЅ‡џџž~џ§šyџр…fџХКЖTЦУТTтЭЦџўфлџўпеџХ­ІтттттттЦŽ|тџІˆџў €џс‹nџХЛЗTЦУТTтЫФџХГ­тттттттЦŠvтрsџХЛИT88888888ђрР€€РР€€Рс‡ѓЯ( @ €чччФФФ4OOO•JJJšyyy<ЮЮЮ GED‰{rmш~~~ўuuuэMMM9ЦЦЦ=ЩЩЩ<щщщ qqq8Њ888qРРР+`[WЫ‡‡†§ЋЋЋўIIImMMMztibљГyўuokўzzzўWTRдkfdчhhhхiiipЦЦЦ8fYџўДœџЦˆsџ„ТТТ8|nfт{oў[TOўKKKТ є?95ўveў‘wfўQKGўFCBўf\Uў`ZVўNNNšЦЦЦ8riтўЛЅџўЙЁџџБ˜џЦŠvџqННН YQLФЙ”~ўˆxmў777ћ222§410ў876ў<:9ў444ў,,,ў#!ўXNHцAЦЦЦ8laџўС­џўНЉџўЛЄџўЖžџџА–џЦ‡rџ„‡‡‡(===Ї---ў333ўRJDў—}mўМ”{ў•yfўyi_ў–|kў]RJў642ўk^Vю`ZVЙ^^^AЛЛЛЦЦЦ8woтџШЖџўФАџўРЌџўНЈџўИЁџџЕœџџЏ•џЄscџ_[XЫh_Yї>=<ўqaXўГwўЦ›ўЧ›ўЧ›ўЦ›ўШœ€ў’wfў=:8ўЙ–ўЏŽyўqjfрWUTАmmmHЫЫЫЦЦЦ8tlџўЯОџўЪИџўЧЕџўУАџўС­џўМІџўИ џџВ˜џЇ…zџ~ibћC:6ЙXPMвœ~mўЧœ€ўЧ›ўЧ›ўЧ›ўЧ›ўЧ›ўДwўF>9ўsaVўЋyўИ“|ў‚pdўre]њa^[ЫЦЦЦ8ztтўгХџўбТџўЮОџўЪИџўФБџўУАџўОЉџўКЄџџДœџьЇџюЃŠџЦ…pџF* џœeRўЌyўЧ›€ўЧ›ўЧ›ўЧ›ўЧ›ўК–ўl`YўnPFў‚h_ўЏŽzў›ƒuў}rўkfcўqqq8ztџџмбџўзЪџўдЦџўбТџўЮНџўЩЗџўУАџўПЋџўЛЅџўИЁџџГšџўЗŸџўИЁџЦЦЦџ‹]Mџ›fRў›zi§Ќ‹uўЕ‘zўЕyўЉŠvўˆl`ќАdJ§ёtJџТbBџrhcўЊІЄў~~~ў“““ўЊўнгџџмбџўкЯџўиЫџўгХџўаСџўЫКџўЦГџўРЌџўНЉџўИЁџџПЊџўХВџўЋџџФБџЦЦЦџL:џЦx_џ–n`ўpdўŽpdў lZўЯoPџэxQџђxOџчqIџ†VFўš—–ўƒƒƒўlllУ888qХЎІџўнвџџмбџўйЭџўдЧџўвУџўЭНџўЪИџўФАџўПЋџўаСџўйЮџўЛЄџўВ˜џў‹eџ§Г›џЦЦЦџ‹\MџљŽkџјŒiџї‡bџі„_џѕ[џѕXџѓ{SџђyPџхpHџƒZLџ   zzz:Š‰ЦЋЃџџмбџўйЭџўжЪџџдЦџўЯПџўЬКџўЦДџўеЧџўукџўЮОџўФБџўљїџџФБџў|QџўОЉџЦЦЦџ‹P=џљmџјŠfџї‡bџіƒ]џѕ[џє~Wџѓ{SџМ^>џŒ~yqЦЋЂџўйЭџўеШџџдЦџўбТџўЭНџэсмџы№ѓџўжЩџўС­џўМІџўиЫџўЅˆџџXџќn?џќЋџЦЦЦџ‹\MџљmџјŒiџј‰dџі„_џѕ[џОcDџq‰ˆХЈŸџџдЦџџдЦџўЯПџѓтйџсњџџъчшџўЯПџўЦГџўЙЃџџЌ‘џў›zџўˆaџўvIџћj9џќКЄџЦЦЦџ‹P=џњmџљŒgџї‡bџПfIџŒ{qЦЄšџўгФџљшсџьўџџѓющџўнгџўдЧџўЪЙџўПЋџўД›џџЄ†џџ•rџў€Vџ§qBџљc1џќЇ‹џЦЦЦџ‹\MџћmџТmQџqˆ‡ЦЗВџјџџџїѓяџџ№ыџџющџўжЩџўЮНџџТЎџџИЁџџЉŒџџ›{џџ†^џўlџ§ЕџњoBџќКЄџЦЦЦџS0$џ}‰ˆTGBџЦОМџџшрџџќћџџќћџўйЮџўаСџўЦДџўНЈџџА—џџЂƒџџkџџХВџўєёџ§‡`џž1 џ- џT1&џ}qЦЉ џўзЪџxpџЦДЎџџьхџџъуџўмбџўдЦџўЩЗџўС­џўЖџџЉŒџџ–tџџ’nџ§—vџиS'џ 'џХY6џ§–sџУqVџqŠ‰ХЎІџџмбџўйЭџўжЪџunџЦВЋџџфмџўодџўзЫџўЬЛџўФБџўИЁџџЎ“џџž}џџkџрR$џЅ)џгhEџџ›zџўšxџќ•rџФqUџ}qЦАЈџўржџўнгџўлЯџўзЫџўдЧџwpџЦБЊџўсзџўкЮџўЯОџўЩЗџўНЈџўГšџџЄ…џѓnBџх9џрd:џџŸџџœ{џўšyџ§–sџќ’oџФpUџq‹ŠЦБЊџўуйџўсзџўпдџўмбџўлаџўиЭџўдЧџulџЦАЈџўнвџўпжџџљјџўкЮџўЕžџў†^џўX!џџ‚XџџЃ„џџŸџџž}џў›zџў™vџ§”pџќ‘nџУnRџ}888qЦДЎџџчоџџхнџџфлџўржџўпдџўлаџўиЭџўзЫџўдЦџumџХ­ЅџўпжџџљјџўмаџўІ‰џўzNџў†^џџЇ‰џџЄ…џџŸџџœ{џџ›zџ§™wџ§”pџќ“pџќlџУnRџ888qЊџшрџџшрџџцоџџфлџўуйџўржџўмвџџмбџўйЮџўжЩџўгФџtkџЦЉ џўгФџўВ™џџ”qџџšxџџЊŽџџЇ‰џџЄ…џџ џџŸџў›zџў™wџ§–sџќ”qџќ‘nџќlџЊqqq8€|џџшрџџчпџџфмџџфлџўсзџўпеџўмвџўйЮџўиЬџўеШџўвТџtlџЦ•…џџЄ†џџЄ†џџЌ‘џџЊŽџџЇ‰џџЄ…џџŸџџŸџў›zџ§™wџќ–tџ§”qџќ’pџŒP<џqqq8ЦЦЦ8ƒтџшрџџшрџџхмџџфлџўрзџўржџўмвџџмбџўиЬџўеШџўбСџTE@џЦ…oџџЏ•џџЌ‘џџЊŽџџЇŠџџЃ„џџŸ€џџ|џў›zџ§šyџ§—tџ§”qџŒ^OтЦЦЦ8ЦЦЦ8€|џџшрџџшрџџчоџўукџўрзџўодџџмбџўйЮџўзЪџЦЃ˜џˆ†…‚Ц‡sџџЌ‘џџЊŽџџЇ‰џџЄ…џџŸ€џџŸџ§›zџ§›yџќ˜vџŒS@џЦЦЦ8ЦЦЦ8ƒтџшрџџцнџџфмџўтиџўржџўогџўлЯџХЈŸџqqЦ‡rџџЋџџЉŒџџЄ…џўЁ‚џўŸџў|џ§œ{џŒaSтЦЦЦ8ЦЦЦ8€|џџшрџџцоџџфлџўсиџўржџЦЋЂџ‰ˆ„Ц„oџџЉŒџџЄ…џўЁƒџўŸџ§}џŒVDџЦЦЦ8ЦЦЦ8ƒтџцоџџфлџџфлџХЏЈџqqЦƒlџџЄ…џ§Ђƒџ§Ÿ€џŒcUтЦЦЦ8ЦЦЦ8€{џџфмџЦБЊџŠ‰ƒ€Цiџ§Ђ„џŒYHџЦЦЦ8qqq8Њ888q888qЊqqq8џќџў ќј№рР€€€Рр№јќ?ќ?ј№рР€€€Рр№€јРќр?ў№џјџџџџџ(0` €%@@@`000h777h333=&&&4HHH›ўўpppџ```й "'''4eeeвvvvћ{{{ўtttў<<<”&&&4gggш|pўš}jўzzzў———џ–––ў---|<<<‡aaaбmmmб@@@zџџџSSSЊtg_џ‚€ўЌЌЌўвввў<<<”2RRRсMMMџФ„ўТ™ў~phўQPOџ{{{ўhhhўoaXџzwvўzzzўSSSџ444`џџЖžџџВ™џџЎ“џџ>>>ЊžŠ~џˆyoўRRRў...ўIII”IIIдўџЁ‡vўШўЩžƒўЅŒ{џ???ўIIIўukeџriўVVVўLLLџppp”џўКЃџўКЃџџЖžџџВ™џџЎ“џџ]]]Њ‡reџЦœ‚ўП›ƒўvўYYYђ===љ)))ў555џ1/.ў@;8ў@;7ў2/-џ111ў---ўџTIAў”xfўLLLџџўС­џџНЈџўКЃџўКЃџџВ™џџВ™џџВ™џџ)))>YRN№Е’}ўБzўe`]ў$$$џ888ў211ў50-џ333ў:::ў<<<ў>>>џ333ў666ў(((џўA:5ўQQQ•џўС­џўС­џџНЈџџНЈџўКЃџџЖžџџВ™џџЎ“џџЎ“џџ WWWУ+++ў111ў666ў,,,џKFBўœ„uўШџ’zkўq^QўMMMўaWQџŒvhўXQMў)))џ111ўCCCў>>>ЙVVV†џџХВџџХВџўС­џўС­џџНЈџўКЃџўКЃџџЖžџџВ™џџЎ“џџЎ“џџ333KOOOП444џ///ў111ў000ўrcYџМ˜ўШœ€ўШœ€џШœ€ўШўФ›ўЦœџЩ‚ўЗ‘yў***џSJEўО›„ўР—~џjb]ў___ЬKKKY))),џџЭМџџЭМџџХВџџХВџўС­џўС­џџНЈџўКЃџџЖžџџЖžџџВ™џџЎ“џГqџiiiѕncў~qiџ===ўXSPўЎŽzўШџШœ€ўЧ›ўШœ€џЧ›ўЧ›ўЧ›ўШœ€џШœ€ўТ›‚ўB>;џ555ўЏ|ўШџТœƒўskў~vrў^^^с,,,%џўбСџџЭМџўЩЗџўЩЗџџХВџџХВџўС­џўС­џџНЈџўКЃџџЖžџџВ™џџЎ“џcccџIIIўpppђ!!!ikkk›mfbўЩžƒўШœ€џЧ›ўЧ›ўШœ€џЧ›ўЧ›ўЧ›ўШœ€џЧ›ўШўydVџўzh]ўЩžƒџШўЩ‚ў“{kўKKKџCCCўWWWэVVV•џџдЦџўбСџўбСџџЭМџџЭМџўЩЗџџХВџўС­џўС­џџНЈџўКЃџўКЃџџЖžџџВ™џЌ‹€џЖ†vџџЇ‰џџџ’^Lџvi`ўЩŸƒџЧ›ўЧ›ўШœ€џЧ›ўЧ›ўЧ›ўШœ€џЧ›ўШœ€ўД—„џ-,+ўў]]]џ~tnўТšўЏ“ўЪŸ„џЫЂˆўЖ”~ўZZZўџџдЦџџдЦџџдЦџўбСџўбСџџЭМџўЩЗџџХВџџХВџџХВџўС­џџНЈџўКЃџџЖžџџВ™џџЎ“џџЎ“џџЊŽџџЎ“џџѓŽkџ†RAџŒ{pџХœ‚ўШœ€ўШœ€џЧ›ўЧ›ўЧ›ўШœ€џЧ›ўЧ‚ў•‚vџg\Xџ}^TџЌcKџzhbџЇ‰wў­‹vўwojџvniўrlhў___ўџџмбџџмбџўиЬџџдЦџџдЦџўбСџўбСџџЭМџўЩЗџџХВџўС­џўС­џџНЈџўКЃџўКЃџџЖžџџВ™џџЎ“џўКЃџџЖžџџџџџџњ’oџxM>џ€maќТ™€ўЦœџШœ€ўШœ€ўШœ€ўШœ€џХœƒў™ƒvўZZZїИ_BџёtJџ№qGџŸU=џ{qkўzmdўоннџŒŒŒўВВВўŒŒўџџмбџџмбџџмбџўиЬџўиЬџџдЦџџдЦџўбСџџЭМџџЭМџўЩЗџџХВџўС­џџНЈџџНЈџўКЃџџЖžџџВ™џџХВџџНЈџўКЃџџџџџџџџџџњ’oџЁeQџfUPџoc\џŒvhў”~qў“~qўˆqcџea_ўyTGџРgJџђwNџђwNџёtJџХdCџ\ROџlllў’’’џ@@@ўaaaўœœœўџўржџџмбџџмбџџмбџџмбџўиЬџџдЦџџдЦџўбСџџЭМџўЩЗџџХВџўС­џўС­џџНЈџўКЃџџЖžџџЭМџўЩЗџўС­џў”pџџ|Qџџџџџџџџџџњ’oџцƒcџЏp\џ“shџˆg[џˆf[џ™wlџЧlNџыyTџѕ~VџѓzRџѓ{SџђwNџёtJџКZ:џUHDџЬЬЬџЁЁЁўžžžў>>>{џўржџўржџџмбџџмбџџмбџўиЬџџдЦџџдЦџўбСџџЭМџџЭМџўЩЗџџХВџўС­џџНЈџџНЈџџмбџўиЬџџЭМџџŸџџkџџ„[џќvJџџџџџџџџџџћ‘nџљŽkџљŽkџј‹gџїˆcџї„^џі„_џѕ[џѕ[џѕ~VџѓzRџѓ{SџђwNџёtJџН_?џ~yџlllџ|||ЧџўржџџмбџџмбџџмбџўиЬџўиЬџџдЦџџдЦџўбСџџЭМџўЩЗџџХВџџХВџўС­џџфлџўржџўиЬџџЎ“џўЩЗџџџџџџЖžџџ|QџќvJџџџџџџџџџџњ’oџљŽkџљŽkџј‹gџїˆcџї„^џі„_џѕ[џѕ[џѕ~VџѓzRџѓ{SџђwNџ№tKџџџџмбџџмбџџмбџўиЬџўиЬџџдЦџџдЦџўбСџџЭМџџЭМџўЩЗџџХВџџыхџџфлџџфлџўКЃџџВ™џџџџџџџџџџџџџџ„[џџ|Qџўn>џџџџџџџџџџћ‘nџњ’oџј‹gџљ‹fџїˆcџї„^џї„^џѕ[џѕ[џѕ~VџѓzRџѓ{SџџџџмбџџмбџўиЬџўиЬџџдЦџџдЦџџдЦџўбСџџЭМџџХВџлєџџџыхџџшрџџХВџџНЈџџЖžџўбСџџџџџўС­џџ„[џџ|Qџўn>џњh7џџџџџџџџџџћ‘nџњ’oџј‹gџї‹hџј‹gџї„^џі„_џѕ[џѕ[џѕ~VџџџџмбџўиЬџџдЦџџдЦџџдЦџўбСџџЭМџџЭМџх§џџлєџџлєџџџЭМџўЩЗџўС­џўКЃџџВ™џџЇ‰џџ›zџџkџџ„[џќvJџўn>џњh7џџџџџџџџџџћ‘nџњ’oџљŽkџљ‹fџј‹gџї„^џі„_џѕ[џџџўиЬџџдЦџџдЦџџдЦџўбСџџЭМџх§џџх§џџлєџџўиЬџџдЦџџЭМџџХВџџНЈџџЖžџџЊŽџџЃ„џў”pџџ„[џџ|Qџўn>џњh7џљd3џџџџџџџџџџћ‘nџњ’oџњŽjџњŠeџїˆcџї„^џџџџдЦџџдЦџџдЦџџЭМџђџџџх§џџх§џџўржџџмбџўиЬџџЭМџўЩЗџўС­џўКЃџџЎ“џџЇ‰џџ›zџџkџџ„[џќvJџўn>џљd3џљd3џџџџџџџџџџћ‘nџќ‘nџљŽkџљ‹fџџџџдЦџўбСџћџџџђџџџђџџџџфлџџфлџџмбџўиЬџўбСџўЩЗџџХВџџНЈџџЖžџџЊŽџџЃ„џџ“pџџ„[џџ|Qџўn>џњh7џџVџљd3џџџџџџџџџџћ‘nџќ‘nџџџџџџџћџџџђџџџџфлџџѓяџџџџџџѓяџўиЬџџдЦџџЭМџџХВџџНЈџџЖžџџЎ“џџЃ„џџ›zџџŒeџџ„[џџЎ“џџџџџ§ џљd3џљd3џџџџџџџџџџџџџџџџџџшрџџшрџџџџџџџџџџџџџџмбџџдЦџџЭМџўЩЗџўС­џўКЃџџВ™џџЊŽџџ›zџџ“pџџ„[џџџџџџџџџџџџџџVџџVџџџџџџўиЬџџдЦџџџшрџџшрџџѓяџџџџџџѓяџџмбџўиЬџўбСџўЩЗџџХВџџНЈџџЖžџџЎ“џџЃ„џџ—uџџŒeџџВ™џџџџџўЃ…џљd3џuџ­+џuџў˜uџћ‘nџџџџмбџўиЬџўиЬџџдЦџџџшрџџшрџџфлџџфлџўржџўиЬџџдЦџџЭМџџХВџўС­џўКЃџџВ™џџЇ‰џџ›zџџ“pџџ„[џќvJџўn>џ­+џ­+џ­+џџ›zџў˜uџ§”qџћ‘nџџџўржџџмбџџмбџўиЬџўиЬџџдЦџџџфлџџшрџџфлџўржџџмбџџдЦџџЭМџўЩЗџўС­џўКЃџџВ™џџЊŽџџŸџџ—uџџŒeџџ|Qџž'џž'џž'џџ›zџџ›zџў›{џў˜uџќ”rџ§lџџџўржџўржџџмбџџмбџўиЬџўиЬџџдЦџџдЦџџџфлџџфлџўржџџмбџўиЬџўбСџўЩЗџџХВџџНЈџџЖžџџЎ“џџЇ‰џџ—uџџkџх9џх9џž'џџŸџџŸџџ›zџџ›zџў˜uџ§”qџќ”rџќ‘nџџџџфлџўржџўржџўржџџмбџџмбџўиЬџўиЬџџдЦџџдЦџџџфлџџфлџўржџўиЬџўбСџџЭМџўЩЗџўС­џўКЃџџВ™џџЇ‰џџЃ„џх9џх9џх9џџЃ„џџŸџџŸџџ›zџџ›zџў›{џ§”qџ§”qџ§lџќ‘nџџџџфлџџфлџўржџўржџўржџџмбџџмбџџмбџџмбџўиЬџџдЦџџдЦџџџфлџўржџџмбџўиЬџџшрџџџџџўржџўКЃџџВ™џџЊŽџџVџџVџџVџџЃ„џџЃ„џџŸџџŸџџŸџџ›zџў›{џў˜uџў”pџќ”rџќ‘nџћiџџџџшрџџфлџџфлџџфлџџфлџўржџўржџџмбџџмбџўиЬџўиЬџўиЬџџдЦџўбСџџўржџџмбџўиЬџџџџџџџџџџџџџџНЈџџЖžџџ|Qџўn>џџVџџЇ‰џџЇ‰џџЃ„џџŸџџŸџџ›zџџ›zџџ›zџў˜uџ§”qџ§”qџ§lџќ‘nџћiџџџџшрџџшрџџшрџџшрџџфлџџфлџўржџўржџўржџџмбџџмбџўиЬџўиЬџўиЬџџдЦџўбСџџўржџўиЬџџшрџџџџџўржџўС­џџŒeџџ|Qџџ|QџџЊŽџџЇ‰џџЇ‰џџЃ„џџŸџџŸџџ›zџџ›zџџ›zџ§˜vџ§”qџў”pџќ”rџ§lџ§lџћiџџџџшрџџшрџџшрџџшрџџфлџџфлџџфлџўржџўржџџмбџџмбџџмбџџмбџўиЬџџдЦџџдЦџўбСџџџмбџўиЬџўбСџўЩЗџџ›zџџkџџ„[џџЊŽџџЊŽџџЇ‰џџЇ‰џџЃ„џџЃ„џџŸџџŸџџ›zџџ›zџў˜uџў˜uџ§”qџќ”rџќ‘mџќ‘nџ§lџџџџшрџџшрџџшрџџшрџџфлџџфлџџфлџџфлџўржџўржџџмбџџмбџўиЬџўиЬџўиЬџџдЦџџдЦџўбСџџўиЬџўбСџџЊŽџџ›zџџ“pџџЎ“џџЊŽџџЊŽџџЇ‰џџЇ‰џџЃ„џџŸџџŸџџŸџџ›zџў›{џ§˜vџў˜uџ§”qџ§”qџќ”rџ§lџ§lџџџџшрџџшрџџшрџџшрџџфлџџфлџџфлџўржџўржџўржџџмбџџмбџўиЬџўиЬџўиЬџџдЦџўбСџўбСџџџВ™џџЇ‰џџŸџџВ™џџЎ“џџЊŽџџЊŽџџЇ‰џџЇ‰џџЃ„џџŸџџŸџџŸџџ›zџџ›zџ§˜vџќ˜wџ§”qџ§”qџќ”rџ§lџџџџшрџџшрџџшрџџшрџџфлџџфлџџфлџўржџўржџўржџџмбџџмбџџмбџўиЬџўиЬџџдЦџўбСџўбСџџџЇ‰џџВ™џџЎ“џџЎ“џџЊŽџџЊŽџџЊŽџџЃ„џџЃ„џџŸџџŸџџ›zџџ›zџ§œ{џў›{џ§˜vџ§”qџ§”qџќ”rџџџџшрџџшрџџшрџџшрџџшрџџфлџџфлџўржџўржџўржџџмбџџмбџўиЬџўиЬџџдЦџўбСџўбСџџџџВ™џџЎ“џџЎ“џџЊŽџџЊŽџџЇ‰џџЇ‰џџЃ„џџЃ„џџŸџџŸџџ›zџ§œ{џ§˜vџ§˜vџў˜uџ§”qџџџџшрџџшрџџшрџџшрџџшрџџфлџџфлџўржџўржџџмбџџмбџџмбџўиЬџўиЬџџдЦџџџџЎ“џџЎ“џџЊŽџџЊŽџџЇ‰џџЇ‰џџЃ„џџŸџџŸџџŸџў›{џ§œ{џ§œ{џќ˜wџќ˜wџџџџшрџџшрџџшрџџфлџџфлџџфлџўржџўржџўржџџмбџџмбџўиЬџўиЬџџџџЎ“џџЎ“џџЊŽџџЊŽџџЇ‰џџЃ„џџЃ„џўŸ€џџŸџџŸџ§œ{џ§œ{џ§œ{џџџџшрџџшрџџшрџџшрџџфлџџфлџўржџўржџўржџџмбџџмбџџџџЎ“џџЊŽџџЊŽџџЇ‰џџЃ„џџЃ„џўŸ€џџŸџў›{џ§œ{џ§œ|џџџџшрџџшрџџшрџџфлџџфлџџфлџўржџўржџџмбџџџџЊŽџџЊŽџџЇ‰џџЃ„џўЃ…џўŸ€џџŸџ§ €џў›{џџџџшрџџшрџџфлџџфлџџфлџџфлџўржџџџџЊŽџџЇ‰џџЃ„џўЃ…џ§ €џўŸ€џ§ џџџџшрџџфлџџфлџџфлџџфлџџџџЇ‰џџЃ„џўЃ…џ§ €џ§ €џџџџшрџџфлџџфлџџџџЇ‰џўЃ…џўЃ…џџџџџџџџџџџ џџџќџјќ?џ№|?џр<?џРџ€ џўќј№рРРРр№јќ?ўџџџ€џџРџџРџџ€џџџўќ?ј№рРРРр№јќ€?ўРџрџџ€№џџРјџџр?ќџџ№ўџџјџџџџџџџџџџџџџџџtortoisehg-4.5.2/icons/scalable/0000755000175000017500000000000013251112740017400 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/scalable/status/0000755000175000017500000000000013251112740020723 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/scalable/status/hg-patch-applied.svg0000644000175000017500000004667413150123225024572 0ustar sborhosborho00000000000000 image/svg+xml tortoisehg-4.5.2/icons/scalable/status/thg-subrepo.svg0000644000175000017500000001420313150123225023701 0ustar sborhosborho00000000000000 unsorted Open Clip Art Library, Source: Wiki Commons, Source: Wikimedia Commons image/svg+xml en S tortoisehg-4.5.2/icons/scalable/status/thg-error.svg0000644000175000017500000001342613150123225023361 0ustar sborhosborho00000000000000 Error image/svg+xml Error Yuki Kodama TortoiseHg Project 2010-05-29 tortoisehg-4.5.2/icons/scalable/status/hg-patch-guarded.svg0000644000175000017500000003011413150123225024545 0ustar sborhosborho00000000000000 QGuard icon image/svg+xml QGuard icon 2011-01-28 Patrice LACOUTURE QGuard icon for TortoiseHg Patrice LACOUTURE tortoisehg-4.5.2/icons/scalable/status/hg-patch-unguarded.svg0000644000175000017500000003146313150123225025120 0ustar sborhosborho00000000000000 QGuard icon image/svg+xml QGuard icon 2011-01-28 Patrice LACOUTURE QGuard icon for TortoiseHg Patrice LACOUTURE tortoisehg-4.5.2/icons/scalable/status/thg-git-subrepo.svg0000644000175000017500000001112113150123225024456 0ustar sborhosborho00000000000000 unsorted Open Clip Art Library, Source: Wiki Commons, Source: Wikimedia Commons image/svg+xml en tortoisehg-4.5.2/icons/scalable/status/hg-merged-p1.svg0000644000175000017500000002606713150123225023632 0ustar sborhosborho00000000000000 view diff image/svg+xml view diff 2011-02-16 Peer Sommerlund tortoisehg-4.5.2/icons/scalable/status/hg-modified.svg0000644000175000017500000001121013150123225023611 0ustar sborhosborho00000000000000 image/svg+xml Commit 2008-04-09 Peer Sommerlund Commit icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/status/thg-svn-subrepo.svg0000644000175000017500000001524113150123225024510 0ustar sborhosborho00000000000000 unsorted Open Clip Art Library, Source: Wiki Commons, Source: Wikimedia Commons image/svg+xml en tortoisehg-4.5.2/icons/scalable/status/thg-warning.svg0000644000175000017500000001547113150123225023677 0ustar sborhosborho00000000000000 Warning image/svg+xml Warning Yuki Kodama TortoiseHg Project 2010-05-31 tortoisehg-4.5.2/icons/scalable/status/thg-remote-repo.svg0000644000175000017500000024767613150123225024506 0ustar sborhosborho00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz HTML hypertext web tortoisehg-4.5.2/icons/scalable/status/thg-success.svg0000644000175000017500000001173713150123225023703 0ustar sborhosborho00000000000000 Success image/svg+xml Success Yuki Kodama 2010-05-29 TortoiseHg Project tortoisehg-4.5.2/icons/scalable/status/thg-added-subrepo.svg0000644000175000017500000002100313150123225024734 0ustar sborhosborho00000000000000 unsorted Open Clip Art Library, Source: Wiki Commons, Source: Wikimedia Commons image/svg+xml en S tortoisehg-4.5.2/icons/scalable/status/hg-sharedrepo.svg0000644000175000017500000001263413150123225024200 0ustar sborhosborho00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz Symbolic Link emblem symbolic link pointer io file tortoisehg-4.5.2/icons/scalable/status/hg-merged-p2.svg0000644000175000017500000002737613150123225023637 0ustar sborhosborho00000000000000 view diff image/svg+xml view diff 2011-02-16 Peer Sommerlund tortoisehg-4.5.2/icons/scalable/status/hg-merged-both.svg0000644000175000017500000002213413150123225024235 0ustar sborhosborho00000000000000 view diff image/svg+xml view diff 2011-02-16 Peer Sommerlund tortoisehg-4.5.2/icons/scalable/status/thg-removed-subrepo.svg0000644000175000017500000002203613150123225025343 0ustar sborhosborho00000000000000 unsorted Open Clip Art Library, Source: Wiki Commons, Source: Wikimedia Commons image/svg+xml en S tortoisehg-4.5.2/icons/scalable/actions/0000755000175000017500000000000013251112740021040 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/scalable/actions/edit-cut.svg0000644000175000017500000005501313150123225023301 0ustar sborhosborho00000000000000 image/svg+xml Edit Cut Garrett Le Sage edit cut clipboard Jakub Steiner tortoisehg-4.5.2/icons/scalable/actions/thg-shelve-move-left-file.svg0000644000175000017500000002427213150123225026445 0ustar sborhosborho00000000000000 image/svg+xml Media Playback Start Lapo Calamandrei play media music video player Jakub Steiner tortoisehg-4.5.2/icons/scalable/actions/hg-verify.svg0000644000175000017500000003774613150123225023500 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/scalable/actions/hg-commit.svg0000644000175000017500000000472213150123225023450 0ustar sborhosborho00000000000000 image/svg+xml tortoisehg-4.5.2/icons/scalable/actions/hg-status.svg0000644000175000017500000002514613150123225023506 0ustar sborhosborho00000000000000 image/svg+xml Status 2008-04-16 Peer Sommerlund Icon for TortoiseHg dialog "File Status" image/svg+xml tortoisehg-4.5.2/icons/scalable/actions/thg-shelve-move-right-all.svg0000644000175000017500000003452513150123225026463 0ustar sborhosborho00000000000000 image/svg+xml Media Seek Forward Lapo Calamandrei Jakub Steiner tortoisehg-4.5.2/icons/scalable/actions/thg-shelve-delete-right.svg0000644000175000017500000000655713150123225026215 0ustar sborhosborho00000000000000 Delete-files-right image/svg+xml Delete-files-right 2011-01-09 Peer Sommerlund tortoisehg-4.5.2/icons/scalable/actions/thg-qreorder.svg0000644000175000017500000001443713150123225024173 0ustar sborhosborho00000000000000 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-4.5.2/icons/scalable/actions/view-filter.svg0000644000175000017500000003661413150123225024026 0ustar sborhosborho00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz tortoisehg-4.5.2/icons/scalable/actions/qimport.svg0000644000175000017500000004416313150123225023262 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/scalable/actions/hg-purge.svg0000644000175000017500000003414513150123225023304 0ustar sborhosborho00000000000000 image/svg+xml tortoisehg-4.5.2/icons/scalable/actions/thg-shelve-delete-left.svg0000644000175000017500000000666113150123225026026 0ustar sborhosborho00000000000000 Delete-files-left image/svg+xml Delete-files-left 2011-01-09 Peer Sommerlund tortoisehg-4.5.2/icons/scalable/actions/hg-qpush-all.svg0000644000175000017500000001430713150123225024066 0ustar sborhosborho00000000000000 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-4.5.2/icons/scalable/actions/hg-qpush.svg0000644000175000017500000001042613150123225023316 0ustar sborhosborho00000000000000 QPush image/svg+xml QPush 2010-10-17 Peer Sommerlund QPush icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/hg-bookmarks.svg0000644000175000017500000001537113150123225024152 0ustar sborhosborho00000000000000 image/svg+xml 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/hg-shelve.svg0000644000175000017500000002505013150123225023443 0ustar sborhosborho00000000000000 image/svg+xml Status 2008-04-16 Peer Sommerlund Icon for TortoiseHg dialog "File Status" image/svg+xml tortoisehg-4.5.2/icons/scalable/actions/hg-log.svg0000644000175000017500000002226713150123225022745 0ustar sborhosborho00000000000000 image/svg+xml Log 2008-05-19 Peer Sommerlund Log icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/hg-export.svg0000644000175000017500000004767613150123225023520 0ustar sborhosborho00000000000000 image/svg+xml Tuomas Kuosmanen http://www.tango-project.org save document store file io floppy media Jakub Steiner tortoisehg-4.5.2/icons/scalable/actions/thg-mq.svg0000644000175000017500000001233513150123225022760 0ustar sborhosborho00000000000000 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-4.5.2/icons/scalable/actions/thg-shelve-move-right-file.svg0000644000175000017500000002564213150123225026632 0ustar sborhosborho00000000000000 image/svg+xml Media Playback Start Lapo Calamandrei play media music video player Jakub Steiner tortoisehg-4.5.2/icons/scalable/actions/hg-bisect-bad-good.svg0000644000175000017500000004232113150123225025100 0ustar sborhosborho00000000000000 image/svg+xml Garrett Le Sage edit cut clipboard Jakub Steiner tortoisehg-4.5.2/icons/scalable/actions/hg-undo.svg0000644000175000017500000001270313150123225023123 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-18 Peer Sommerlund Revert icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/new-group.svg0000644000175000017500000002162513150123225023510 0ustar sborhosborho00000000000000 image/svg+xml 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/status-check.svg0000644000175000017500000003647113150123225024170 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/scalable/actions/hg-update.svg0000644000175000017500000007721513150123225023451 0ustar sborhosborho00000000000000 Update image/svg+xml Update Yuki Kodama 2010-05-29 TortoiseHg Project tortoisehg-4.5.2/icons/scalable/actions/hg-import.svg0000644000175000017500000004771313150123225023501 0ustar sborhosborho00000000000000 image/svg+xml Media Floppy Tuomas Kuosmanen http://www.tango-project.org save document store file io floppy media Jakub Steiner tortoisehg-4.5.2/icons/scalable/actions/thg-guess.svg0000644000175000017500000002263213150123225023472 0ustar sborhosborho00000000000000 image/svg+xml Status 2008-04-16 Peer Sommerlund Icon for TortoiseHg dialog "File Status" image/svg+xml tortoisehg-4.5.2/icons/scalable/actions/hg-qpush-move.svg0000644000175000017500000001270013242076403024266 0ustar sborhosborho00000000000000 QReorder icon image/svg+xml QReorder icon 2011-01-25 Richard Marti QPushMove icon for TortoiseHg - derived from QPush icon by Peer Sommerlund Peer Sommerlund, Patrice LACOUTURE, Richard Marti tortoisehg-4.5.2/icons/scalable/actions/hg-init.svg0000644000175000017500000002272513150123225023126 0ustar sborhosborho00000000000000 image/svg+xml Initialize 2008-12-27 Peer Sommerlund Icon for TortoiseHg dialog "Init" tortoisehg-4.5.2/icons/scalable/actions/thg-sync.svg0000644000175000017500000001446113150123225023321 0ustar sborhosborho00000000000000 image/svg+xml Sync 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/ldiff.svg0000644000175000017500000004312113150123225022644 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/scalable/actions/go-up.svg0000644000175000017500000001777313150123225022625 0ustar sborhosborho00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Up go higher up arrow pointer > Andreas Nilsson tortoisehg-4.5.2/icons/scalable/actions/hg-recover.svg0000644000175000017500000003772513150123225023636 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/scalable/actions/hg-serve.svg0000644000175000017500000003330513150123225023303 0ustar sborhosborho00000000000000 image/svg+xml Proxy 2008-04-19 Peer Sommerlund Icon for TortoiseHg dialog "Web Server" image/svg+xml tortoisehg-4.5.2/icons/scalable/actions/edit-find.svg0000644000175000017500000010512413150123225023425 0ustar sborhosborho00000000000000 image/svg+xml Edit Find edit find locate search Steven Garrity Jakub Steiner tortoisehg-4.5.2/icons/scalable/actions/go-home.svg0000644000175000017500000005013113150123225023112 0ustar sborhosborho00000000000000 image/svg+xmlGo HomeJakub Steinerhttp://jimmac.musichall.czhomereturngodefaultuserdirectoryTuomas Kuosmanen tortoisehg-4.5.2/icons/scalable/actions/view-annotate.svg0000644000175000017500000001140713150123225024343 0ustar sborhosborho00000000000000 view annotations image/svg+xml view annotations 2011-02-16 Peer Sommerlund # tortoisehg-4.5.2/icons/scalable/actions/mail-forward.svg0000644000175000017500000011027113150123225024145 0ustar sborhosborho00000000000000 image/svg+xml Mail Jakub Steiner Andreas Nilsson mail e-mail MUA tortoisehg-4.5.2/icons/scalable/actions/application-exit.svg0000644000175000017500000002343613150123225025041 0ustar sborhosborho00000000000000 ]> tortoisehg-4.5.2/icons/scalable/actions/copy-patch.svg0000644000175000017500000004062413150123225023634 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/scalable/actions/thg-reporegistry.svg0000644000175000017500000012713213150123225025103 0ustar sborhosborho00000000000000 repotree image/svg+xml repotree Johan Samyn TortoiseHg Project 2010-09-01 tortoisehg-4.5.2/icons/scalable/actions/hg-pull.svg0000644000175000017500000004010313150123225023125 0ustar sborhosborho00000000000000 image/svg+xml 2011-02-14 Peer Sommerlund Pull icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/thg-ignore.svg0000644000175000017500000001613313150123225023626 0ustar sborhosborho00000000000000 image/svg+xml Ignore 2009-02-28 Peer Sommerlund "Ignore" icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/thg-log-load-all.svg0000644000175000017500000003001313150123225024600 0ustar sborhosborho00000000000000 loadall image/svg+xml loadall Johan Samyn TortoiseHg Project 2010-09-01 N 0 tortoisehg-4.5.2/icons/scalable/actions/hg-qguard.svg0000644000175000017500000003162013150123225023440 0ustar sborhosborho00000000000000 QGuard icon image/svg+xml QGuard icon 2011-01-28 Patrice LACOUTURE QGuard icon for TortoiseHg Patrice LACOUTURE tortoisehg-4.5.2/icons/scalable/actions/hg-pull-to-here.svg0000644000175000017500000006374713150123225024511 0ustar sborhosborho00000000000000 image/svg+xml 2011-02-14 Peer Sommerlund Pull icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/process-stop.svg0000644000175000017500000002750213150123225024226 0ustar sborhosborho00000000000000 image/svg+xml Stop 2005-10-16 Andreas Nilsson stop halt error Jakub Steiner tortoisehg-4.5.2/icons/scalable/actions/hg-clone.svg0000644000175000017500000011736213150123225023265 0ustar sborhosborho00000000000000 Clone image/svg+xml Clone Yuki Kodama TortoiseHg Project 2010-05-29 tortoisehg-4.5.2/icons/scalable/actions/hg-qpop.svg0000644000175000017500000001056713150123225023143 0ustar sborhosborho00000000000000 QPop image/svg+xml QPop 2010-10-17 Peer Sommerlund QPop icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/thg-shelve-move-right-chunks.svg0000644000175000017500000002452313150123225027203 0ustar sborhosborho00000000000000 Chunk-to-right image/svg+xml Chunk-to-right Peer Sommerlund Lapo Calamandrei, Jakub Steiner 2011-01-09 tortoisehg-4.5.2/icons/scalable/actions/hg-qdelete.svg0000644000175000017500000002325513150123225023605 0ustar sborhosborho00000000000000 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-4.5.2/icons/scalable/actions/hg-extensions.svg0000644000175000017500000001275013150123225024357 0ustar sborhosborho00000000000000 Extensions image/svg+xml Extensions 2010-11-13 Johan Samyn Extensions icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/view-hidden.svg0000644000175000017500000004760013150123225023771 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/scalable/actions/thg-sync-bookmarks.svg0000644000175000017500000002200413150123225025277 0ustar sborhosborho00000000000000 image/svg+xml Sync 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/compare-files.svg0000644000175000017500000004366713150123225024325 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" = ? tortoisehg-4.5.2/icons/scalable/actions/qfinish.svg0000644000175000017500000004425513150123225023232 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/scalable/actions/copy-hash.svg0000644000175000017500000004554613150123225023470 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" # # tortoisehg-4.5.2/icons/scalable/actions/view-refresh.svg0000644000175000017500000001560413150123225024173 0ustar sborhosborho00000000000000 ]> tortoisehg-4.5.2/icons/scalable/actions/thg-password.svg0000644000175000017500000004560213150123225024210 0ustar sborhosborho00000000000000 tortoisehg-4.5.2/icons/scalable/actions/go-to-rev.svg0000644000175000017500000003157413150123225023410 0ustar sborhosborho00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz go jump seek arrow pointer tortoisehg-4.5.2/icons/scalable/actions/document-new.svg0000644000175000017500000004177613150123225024203 0ustar sborhosborho00000000000000 image/svg+xml New Document Jakub Steiner http://jimmac.musichall.cz tortoisehg-4.5.2/icons/scalable/actions/hg-transplant.svg0000644000175000017500000000735313150123225024351 0ustar sborhosborho00000000000000 image/svg+xml tortoisehg-4.5.2/icons/scalable/actions/thg-console.svg0000644000175000017500000010274213150123225024007 0ustar sborhosborho00000000000000 showlog image/svg+xml showlog Johan Samyn TortoiseHg Project 2010-09-01 tortoisehg-4.5.2/icons/scalable/actions/view-file.svg0000644000175000017500000001101713150123225023446 0ustar sborhosborho00000000000000 View changes in file image/svg+xml View changes in file 2011-02-16 Peer Sommerlund tortoisehg-4.5.2/icons/scalable/actions/thg-qrefresh.svg0000644000175000017500000005405313150123225024165 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/scalable/actions/hg-qfold.svg0000644000175000017500000002645013150123225023267 0ustar sborhosborho00000000000000 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-4.5.2/icons/scalable/actions/hg-merge.svg0000644000175000017500000001300013150123225023244 0ustar sborhosborho00000000000000 image/svg+xml Merge 2008-04-26 Peer Sommerlund tortoisehg-4.5.2/icons/scalable/actions/hg-qpop-all.svg0000644000175000017500000001625313150123225023707 0ustar sborhosborho00000000000000 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-4.5.2/icons/scalable/actions/settings_projrc.svg0000644000175000017500000004237613150123225025012 0ustar sborhosborho00000000000000 image/svg+xml 2011-02-14 Peer Sommerlund Pull icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/go-previous.svg0000644000175000017500000001751313150123225024045 0ustar sborhosborho00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Previous go previous left arrow pointer < tortoisehg-4.5.2/icons/scalable/actions/hg-compress.svg0000644000175000017500000005126013150123225024012 0ustar sborhosborho00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz/ package archive tarball tar bzip gzip zip arj tar jar tortoisehg-4.5.2/icons/scalable/actions/edit-copy.svg0000644000175000017500000003635313150123225023466 0ustar sborhosborho00000000000000 image/svg+xml Edit Copy 2005-10-15 Andreas Nilsson edit copy Jakub Steiner tortoisehg-4.5.2/icons/scalable/actions/hg-grep.svg0000644000175000017500000001316113150123225023112 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/scalable/actions/hg-incoming.svg0000644000175000017500000002710013150123225023756 0ustar sborhosborho00000000000000 image/svg+xml 2011-02-14 Peer Sommerlund Incoming icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/thg-shelve-move-left-all.svg0000644000175000017500000003364013150123225026275 0ustar sborhosborho00000000000000 image/svg+xml Media Seek Backward Lapo Calamandrei tortoisehg-4.5.2/icons/scalable/actions/thg-repository-open.svg0000644000175000017500000002116713150123225025524 0ustar sborhosborho00000000000000 image/svg+xml 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/dialog-warning.svg0000644000175000017500000003352513150123225024471 0ustar sborhosborho00000000000000 image/svg+xml Dialog Warning 2005-10-14 Andreas Nilsson Jakub Steiner, Garrett LeSage dialog warning tortoisehg-4.5.2/icons/scalable/actions/hg-strip.svg0000644000175000017500000001524713150123225023325 0ustar sborhosborho00000000000000 image/svg+xml Remove 2008-05-07 Peer Sommerlund Remove icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/thg-shelve-move-left-chunks.svg0000644000175000017500000003035513150123225027020 0ustar sborhosborho00000000000000 Chunk-to-left image/svg+xml Chunk-to-left Peer Sommerlund Lapo Calamandrei, Jakub Steiner 2011-01-09 tortoisehg-4.5.2/icons/scalable/actions/hg-unbundle.svg0000644000175000017500000004217113150123225023774 0ustar sborhosborho00000000000000 image/svg+xml 2011-02-14 Peer Sommerlund Pull icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/hg-push.svg0000644000175000017500000004012213150123225023131 0ustar sborhosborho00000000000000 image/svg+xml 2011-02-14 Peer Sommerlund Push icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/hg-archive.svg0000644000175000017500000003367613150123225023613 0ustar sborhosborho00000000000000 image/svg+xml Media Floppy Tuomas Kuosmanen http://www.tango-project.org save document store file io floppy media Jakub Steiner tortoisehg-4.5.2/icons/scalable/actions/go-next.svg0000644000175000017500000001731413150123225023146 0ustar sborhosborho00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Next go next right arrow pointer > tortoisehg-4.5.2/icons/scalable/actions/tasktab-refresh.svg0000644000175000017500000002300413150123225024643 0ustar sborhosborho00000000000000 reloadttimage/svg+xmlreloadttJohan SamynTortoiseHg Project2010-08-23 T tortoisehg-4.5.2/icons/scalable/actions/go-jump.svg0000644000175000017500000001766213150123225023151 0ustar sborhosborho00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Jump go jump seek arrow pointer tortoisehg-4.5.2/icons/scalable/actions/thg-add-subrepo.svg0000644000175000017500000002017713150123225024553 0ustar sborhosborho00000000000000 unsorted Open Clip Art Library, Source: Wiki Commons, Source: Wikimedia Commons image/svg+xml en S tortoisehg-4.5.2/icons/scalable/actions/hg-qgoto.svg0000644000175000017500000003021313150123225023303 0ustar sborhosborho00000000000000 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-4.5.2/icons/scalable/actions/visualdiff.svg0000644000175000017500000003043313150123225023716 0ustar sborhosborho00000000000000 image/svg+xml Repo Browse 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/scalable/actions/hg-rename.svg0000644000175000017500000002160013150123225023421 0ustar sborhosborho00000000000000 image/svg+xml 2010-06-06 Johan Samyn Icon for TortoiseHg dialog "Rename" TortoiseHg Project image/svg+xml tortoisehg-4.5.2/icons/scalable/actions/go-down.svg0000644000175000017500000002015413150123225023133 0ustar sborhosborho00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Down go lower down arrow pointer > Andreas Nilsson tortoisehg-4.5.2/icons/scalable/actions/hg-annotate.svg0000644000175000017500000002237313150123225023773 0ustar sborhosborho00000000000000 Annotate Document image/svg+xml Annotate Document 2008-04-16 Peer Sommerlund Icon for TortoiseHg tab "Annotate" annotate blame image/svg+xml tortoisehg-4.5.2/icons/scalable/actions/hg-bisect-good-bad.svg0000644000175000017500000004560013150123225025103 0ustar sborhosborho00000000000000 image/svg+xml Garrett Le Sage edit cut clipboard Jakub Steiner tortoisehg-4.5.2/icons/scalable/actions/hg-remove.svg0000644000175000017500000001524713150123225023461 0ustar sborhosborho00000000000000 image/svg+xml Remove 2008-05-07 Peer Sommerlund Remove icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/hg-rebase.svg0000644000175000017500000007223113150123225023421 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/scalable/actions/view-diff.svg0000644000175000017500000001302413150123225023437 0ustar sborhosborho00000000000000 view diff image/svg+xml view diff 2011-02-16 Peer Sommerlund tortoisehg-4.5.2/icons/scalable/actions/hg-tag.svg0000644000175000017500000001155513150123225022735 0ustar sborhosborho00000000000000 Tag image/svg+xml Tag 2010-05-29 Yuki Kodama TortoiseHg Project tortoisehg-4.5.2/icons/scalable/actions/view-at-revision.svg0000644000175000017500000005154213150123225024776 0ustar sborhosborho00000000000000 image/svg+xml 2008-05-05 Peer Sommerlund Icon for TortoiseHg dialog "Datamine" tortoisehg-4.5.2/icons/scalable/actions/hg-outgoing.svg0000644000175000017500000002671213150123225024016 0ustar sborhosborho00000000000000 image/svg+xml 2011-02-14 Peer Sommerlund Outgoing icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/hg-revert.svg0000644000175000017500000001262113150123225023464 0ustar sborhosborho00000000000000 image/svg+xml Revert 2008-05-18 Peer Sommerlund Revert icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/hg-bundle.svg0000644000175000017500000004516613150123225023440 0ustar sborhosborho00000000000000 image/svg+xml 2011-02-14 Peer Sommerlund Push icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/actions/edit-file.svg0000644000175000017500000002621513150123225023427 0ustar sborhosborho00000000000000 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-4.5.2/icons/scalable/actions/hg-bisect.svg0000644000175000017500000006770513150123225023443 0ustar sborhosborho00000000000000 image/svg+xml Edit Cut Garrett Le Sage edit cut clipboard Jakub Steiner tortoisehg-4.5.2/icons/scalable/apps/0000755000175000017500000000000013251112740020343 5ustar sborhosborho00000000000000tortoisehg-4.5.2/icons/scalable/apps/thg.svg0000644000175000017500000012272413150123225021654 0ustar sborhosborho00000000000000 image/svg+xml TortoiseHg 2007-dec-11 Peer Sommerlund Closely resembles TortoiseSVN logo Hg tortoisehg-4.5.2/icons/scalable/apps/help-browser.svg0000644000175000017500000003224013150123225023474 0ustar sborhosborho00000000000000 image/svg+xml Help Browser 2005-11-06 Tuomas Kuosmanen help browser documentation docs man info Jakub Steiner, Andreas Nilsson http://tigert.com tortoisehg-4.5.2/icons/scalable/apps/system-file-manager.svg0000644000175000017500000001616313150123225024742 0ustar sborhosborho00000000000000 image/svg+xml 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg tortoisehg-4.5.2/icons/scalable/apps/preferences-desktop-font.svg0000644000175000017500000000763713150123225026013 0ustar sborhosborho00000000000000 fonts.svg image/svg+xml fonts.svg 2010-06-26 Johan Samyn TortoiseHg project F f tortoisehg-4.5.2/icons/scalable/apps/utilities-terminal.svg0000644000175000017500000001312013150123225024703 0ustar sborhosborho00000000000000 image/svg+xml 2008-04-09 Peer Sommerlund Sync icon for TortoiseHg C:>_ tortoisehg-4.5.2/icons/scalable/apps/hg.svg0000644000175000017500000002040313150123225021457 0ustar sborhosborho00000000000000 image/svg+xml tortoisehg-4.5.2/icons/scalable/apps/tools-hooks.svg0000644000175000017500000010337713150123225023356 0ustar sborhosborho00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz preferences settings control panel tweaks system tortoisehg-4.5.2/icons/scalable/apps/tools-spanner-hammer.svg0000644000175000017500000010756613150123225025154 0ustar sborhosborho00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz preferences settings control panel tweaks system tortoisehg-4.5.2/icons/scalable/apps/help-readme.svg0000644000175000017500000007065013150123225023255 0ustar sborhosborho00000000000000 image/svg+xml Generic Text text plaintext regular document Jakub Steiner http://jimmac.musichall.cz tortoisehg-4.5.2/icons/menudelete.ico0000644000175000017500000001373613150123225020465 0ustar sborhosborho00000000000000  Ј6 hо  ˜F( @ џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЃEЉОЈтІrЄџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЄІrЈтЉОЃEџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЇ†ЋєЯџйџ ЛіЇнЊџџџџџџџџџџџџџџџџџџџџџџџџЊЇнКі еџ ЬџЋєЇ†џџџџџџџџџџџџџџџџџџџџџџџџџџџЇ†Џє((фџ**щџ((щџ%%шџФњЇнЊџџџџџџџџџџџџџџџџџџЊЇнТњфџфџфџпџ­єЇ†џџџџџџџџџџџџџџџџџџџџџЇ†Џє,,хџ..ъџ,,ъџ**щџ((щџ%%шџФњЇнЊџџџџџџџџџџџџЊЇнТњфџфџфџфџфџпџ­єЇ†џџџџџџџџџџџџџџџЃEЌє00хџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџФњЇнЊџџџџџџЊЇн УњфџфџфџфџфџфџфџпџЋєЃEџџџџџџџџџџџџЉО##бџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџФњЇнЊЊЇн Уњхџхџфџфџфџфџфџфџфџ ЬџЉОџџџџџџџџџџџџЈт--мџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџХљЇнЇнХљцџцџхџхџфџфџфџфџфџфџ еџЈтџџџџџџџџџџџџІrМі::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџХљХљчџчџцџцџхџхџфџфџфџфџфџКіІrџџџџџџџџџџџџЄЈнЦњ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџ##шџ!!чџчџчџцџцџхџхџфџфџфџТњЇнЄџџџџџџџџџџџџџџџЊЈнЧњ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџ##шџ!!чџчџчџцџцџхџхџфџТњЇнЊџџџџџџџџџџџџџџџџџџџџџЊЇнЦњ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџ##шџ!!чџчџчџцџцџхџ УњЇнЊџџџџџџџџџџџџџџџџџџџџџџџџџџџЊЇнЧњ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџ##шџ!!чџчџчџцџ УњЇнЊџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЊЇнШљ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџ##шџ!!чџчџХљЇнЊџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЊЇнШљ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџ##шџХљЇнЊџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЊЇнШљ<<эџ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџХљЇнЊџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЊЇн!!ЩљAAюџ??юџ<<эџ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџХљЇнЊџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЊЇн""ШњEEяџCCяџAAюџ??юџ<<эџ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџФњЇнЊџџџџџџџџџџџџџџџџџџџџџџџџџџџЊЈн$$ШњII№џGGяџEEяџCCяџAAюџ??юџ<<эџ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџФњЇнЊџџџџџџџџџџџџџџџџџџџџџЊЈн%%ЩњMMёџKK№џII№џGGяџEEяџCCяџAAюџ??юџ<<эџ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџФњЇнЊџџџџџџџџџџџџџџџЄЈн''ЩњQQђџOOёџMMёџKK№џII№џGGяџEEяџCCяџAAюџ??юџ<<эџ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџФњЇнЄџџџџџџџџџџџџІrОіVVѓџSSђџQQђџOOёџMMёџKK№џII№џGGяџEEяџCCяџAAюџШљШљ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџ%%шџ ЛіІrџџџџџџџџџџџџЈтBBсџVVѓџVVѓџSSђџQQђџOOёџMMёџKK№џII№џGGяџEEяџ!!ЩљЇнЇнШљ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџ((щџйџЈтџџџџџџџџџџџџЉО55еџVVѓџVVѓџVVѓџSSђџQQђџOOёџMMёџKK№џII№џ##ШњЇнЊЊЇнЧњ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ,,ъџ**щџЯџЉОџџџџџџџџџџџџЃE ЌєOOэџVVѓџVVѓџVVѓџSSђџQQђџOOёџMMёџ%%ЩњЇнЊџџџџџџЊЇнЦњ::эџ88ьџ66ьџ44ыџ22ыџ00ыџ..ъџ((фџЋєЃEџџџџџџџџџџџџџџџЇ† АєPPэџVVѓџVVѓџVVѓџSSђџQQђџ''ЪњЈнЊџџџџџџџџџџџџЊЇнЧњ::эџ88ьџ66ьџ44ыџ22ыџ,,хџЏєЇ†џџџџџџџџџџџџџџџџџџџџџЇ† АєPPэџVVѓџVVѓџVVѓџ))ЪњЈнЊџџџџџџџџџџџџџџџџџџЊЈнЦњ::эџ88ьџ66ьџ00хџЏєЇ†џџџџџџџџџџџџџџџџџџџџџџџџџџџЇ† ­є;;кџJJшџ""УљЈнЊџџџџџџџџџџџџџџџџџџџџџџџџЊЈнМі--мџ##бџЌєЇ†џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЅOЉйЇьЇЅŸџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџЄІrЈтЉОЃEџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџўўјј№№рррРР€Ррр№јќ?ўџџџџўќ?ј№ррРР€рРрр№№јјў?ўџџџџџџџџ(  ДДџџllџџllџџџџllџџџџ€џHHџџџџ€џџџДДџџџџ€џџџДДџџџџ€џџџДДџџџџ€џџџДДџџџџ€џџџ€џџџДДџџџџ€џџџlџџџџ€џџџџџџџЕџџџџ€џџџllџџllџџџџџџџџ€џџџllџџllџџџџllџџџџllџџџџџџџџџчуЧс‡№јќ?ј№сЯуџџџџџџџ(  ДДџџllџџllџџџџllџџџџ€џHHџџџџ€џџџДДџџ35ц(*цџџ€џџџДДџџџџ€џџџДДџџџџ€џџџДДџџџџ€џџџ€џџџДДџџ&(цџџ€џџџlџџ36чџџ€џџџџџџџЕџџџџ€џџџllџџ01чllџџџџџџџџ€џџџllџџllџџџџllџџџџllџџџ№џ†Р0рp№№рpТ0‡0№џ№tortoisehg-4.5.2/icons/README.txt0000644000175000017500000000276413150123225017337 0ustar sborhosborho00000000000000Some 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) *.png .................... miscellaneous pixmaps *.ico .................... icons used by explorer/nautilus extensions and Windows exe svg/*.svg ................ source of .ico files 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-4.5.2/icons/menuabout.ico0000644000175000017500000000344613150123225020332 0ustar sborhosborho00000000000000 h&  ˜Ž(  @›2џК<џ|џ›2џК<џ|џ›2џК<џ|џ>џ]џ›2џК<џ]џИИИџ›2џ]џ999џ>џ]џ›2џ›2џ]џЄ  џџ|џ]џ999џ>џ>џ›џ]џ]?џ›2џ]?џ›2џ›iџ]?џ>џџRRRџ>*џ]?џ||џ››џ››џ››џ___џ]?џ]?џ>џџ>џ>џ999џ››џџЮlџ››џџlџй“џџlџ›iџ]?џйџ>џџ]џК<џPxxxџй“џџЮlџџЮlџХХХџџЮlџџЮlџџllџ]?џџllџйџ№ЊџК<џ№ЊџRRRџ’’’џџкџџџŒџџкџХХХџ’’’џ999џP№ЊџџџйџEEEџlllџџџŒџџllџ___џ999џКџџџџџџџџџК<џP999џEEEџP€џйџйџ€џ€џ€џ€џ€џ€џ€џ€џ€џ€џ€џ€џќŒ„€Р€р№0јiџяџчџѓџ“џЧ(  p•H*—>%?Š7?‘!+В- џz Яy0?…A™@2?Њўўta_“%/ ўSџMў€=!?—A4?ž+ўn ў‡…я_пfў:**џAўz>$?V7ƒ0ŸfџrMџƒQџNџ€\џQ'џ џrE*/a*ŸA-"ўk^ўЕЂ(џД'ўО’ўЂa^џlIўў+ п[)yA"–?џwooўБ‰ўјЪeџмЗ†ўьЛkўјЃnџЁU(ўЯL<ўГ џЌVўЦxЯjSE•Ž€џьо‰џџЮˆџŸŸŸџc^Xп–Œ‚ПЛpџDDDџЎџbSEŸ}}`ў iZўgb]пmQ1?Og_АD3ŸыўЋU.Ÿ]7pW6?oV5?}b>/w П…]/“G#?{R':w{ Яqj,vp4Bwyt4€џBv‚zC4Ÿ~ Я Яvz:ppp0Ррсџќќtortoisehg-4.5.2/icons/settings_repo.ico0000644000175000017500000001246613150123225021222 0ustar sborhosborho00000000000000  Ј& hЮ( @ Р€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџР€DџР€DџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџР€DџЂЦџ ІШџ ЄШџ ХџЂХџР€DџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџР€DџЃЧџЅШџ9ЦпџhешџЁхёџ­яљџё§џР€DџьююџьююџьююџьююџьююџХргџmП•џьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџР€DџЃЦџМйџNщњџuяќџЄє§џвњўџПїўџё§џР€Dџьююџьююџьююџьююџмшуџ%ІdџšOџЕкШџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџР€DџЁФџEсєџPыќџuяќџЃє§џбњўџПїўџё§џР€DџьююџьююџьююџщэьџEБzџšNџ,Јhџ)ІfџцыщџьююџьююџьююџьююџьююџьююџьююџьююџьююџР€DџЁФџFтѕџPыќџtяќџЂє§џбњўџРїўџ‘ё§џР€DџьююџьююџьююџnР—џšNџ7ЋoџхьщџGВ{џwФџьююџьююџьююџьююџьююџьююџьююџьююџьююџР€DџЁФџFтѕџPыќџtяќџЂє§џањўџРїўџ‘ё§џР€DџьююџьююџьююџfН‘џЂ]џдхнџьююџзцпџЃ_џХргџьююџьююџьююџьююџьююџьююџьююџьююџР€DџЁФџFтѕџPыќџsяќџЁє§џањўџСїўџ’ё§џР€DџьююџьююџьююџыюэџЭтиџьююџьююџьююџЇеОџ:­rџъээџьююџьююџьююџьююџьююџьююџьююџР€DџЁФџFтѕџPыќџsяќџЁє§џЯњўџСјўџ“ё§џР€DџьююџьююџьююџьююџьююџьююџьююџьююџьююџcМŽџ‹ЪЊџьююџьююџьююџьююџьююџьююџьююџР€DџЁФџFтѕџPыќџrяќџЁє§џЯњўџТјўџ“ё§џР€DџьююџьююџьююџьююџьююџьююџьююџьююџьююџуышџZЙ‰џьююџьююџьююџьююџьююџьююџьююџР€DџЁФџFтѕџPыќџrяќџ є§џЮљўџТјўџ“ђ§џР€DџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџР€DџЁФџFтѕџPыќџqюќџŸє§џЮљўџУјўџ”ђ§џР€DџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџьююџР€DџЁФџFтѕџPыќџpюќџŸє§џЭљўџУјўџ•ђ§џР€DџЯŸrџЯŸrџЯŸrџЯŸrџЯŸrџЯŸrџЯŸrџЯŸrџЯŸrџаЂvџгЈџжЎˆџйД‘џмК™џпРЂџтЦЋџхЬДџцЮИџР€DџЁФџFтѕџPыќџpюќџžє§џЭљўџФјўџ•ђ§џР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџР€DџЁФџFтѕџPыќџpюќџžє§џЬљўџФјўџ–ђ§џgьќџOщќџOщќџOщќџOщќџOщќџIищџ?ЛЪџ2ЁАџЁХџЁФџFтѕџPыќџoюќџє§џЬљўџХјўџ–ђ§џgьќџOщќџOщќџOщќџOщќџOщќџIиъџ?ЛЪџ2ЁАџЁХџЁФџFтѕџPыќџnюќџє§џЫљўџХјўџ—ђ§џhьќџOщќџOщќџOщќџOщќџOщќџIиъџ?ЛЪџ2ЁАџЁХџЁФџFтѕџPыќџnюќџœє§џЪљўџЦјўџ—ђ§џiьќџOщќџOщќџOщќџOщќџOщќџIиъџ?ЛЪџ2ЁАџЁХџЁФџFтѕџPыќџmюќџœѓ§џЪљўџЧјўџ˜ђ§џiьќџOщќџOщќџOщќџOщќџOщќџIиъџ@МЫџ2ЁАџЁХџЁФџFтѕџPыќџmюќџ›ѓ§џЪљўџЧјўџ˜ђ§џiьќџOщќџOщќџOщќџOщќџOщќџIйъџ@МЫџ2ЁАџЁХџЁФџFтѕџPыќџlюќџ›ѓ§џЩљўџШјўџ—ёќџ]тѕџEрєџNшћџOщќџOщќџOщќџIйъџ@МЫџ2ЁАџЁХџЁФџFтѕџCпѓџ<ЬуџAТлџ5ЙдџЌЬџЂХџЄЧџЄЧџЂХџЋЭџДгџ!Олџ(Рйџ6ИЪџ2ЁАџЁХџЁХџИжџЄЧџ,ЙзџAЦрџVбшџjнёџ~щњџ‹№џџ‹№џџ~щњџjнёџVвщџBЦрџ,КзџЃЦџЃРџЁХџЂЦџ&Ждџ~щњџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџщњџ'ЖдџЂЧџЄЦџIЪуџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџIЪуџЂХџЃХџ!Вбџ]жьџrтєџ…эќџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ‹№џџ…эќџrтѕџ^жьџ"ДгџЁХџ ІЩџЃЦџЁФџЅШџАЯџ0Мйџ0МйџАЯџЅШџЁХџЃЦџ ЅШџџ№џ№џ№ўј№№№№№№№№№№№№џ№џ№џ№џ№џ№џ№џ№џ№џ№џ№џјџўџџџџџџџџџџџџџ(  Ы›nџжЗ™џжЗ™џжЗ™џжЗ™џжЗ™џжЗ™џжЗ™џжЗ™џЫ›nџЂЦ@ ЅШ€ЁХ€жЗ™џьююџьююџьююџьююџьююџьююџьююџьююџжЗ™џ ЋЬП@ачџŸъѕџЂђќџжЗ™џьююџшьыџWЗ†џощфџьююџьююџьююџьююџжЗ™џ#СмџbэќџЙї§џЈє§џжЗ™џьююџhО’џSЖƒџsС™џьююџьююџьююџьююџжЗ™џ#СмџaэќџИї§џЉє§џжЗ™џьююџŽЫЌџцыщџЂгКџЕкШџьююџьююџьююџжЗ™џ#СмџaэќџИї§џЊє§џжЗ™џьююџьююџьююџьююџŠЪЊџьююџьююџьююџжЗ™џ#Смџ`ьќџЖі§џЋѕ§џжЗ™џьююџьююџьююџьююџьююџьююџьююџьююџжЗ™џ#Смџ`ьќџЕі§џЌѕ§џУ‡OџЧ[џЧ[џЧ[џЧ[џШ’_џЫ˜hџЮžpџбЄyџЩ“aџ#Смџ_ьќџДі§џ­ѕ§џ[ъќџOщќџOщќџDЩйџЁКџ#Смџ_ьќџГі§џЎѕ§џ[ъќџOщќџOщќџDЩкџЁКџ#Смџ^ьќџВі§џЏѕ§џ\ъќџOщќџOщќџDЪкџЁКџ#СмџNсѓџvйъџ_Этџ+Тнџ+Чтџ6бщџ9УжџЁКџЌЭџNЭхџkнёџщњџ‹№џџщњџkоёџOЭхџЇШџАЯПeкюџƒыћџ‹№џџ‹№џџ‹№џџƒыћџeкяџЏЯП ІЩ@ЂХ€ЊЫ€0Мй€ЊЫ€ЂХ€ ЅШ@ќЌAрЌAРЌAРЌAРЌAРЌAРЌAРЌAРЌAРЌAРЌAРЌAРЌAРЌAр?ЌAџџЌAtortoisehg-4.5.2/tortoisehg/0000755000175000017500000000000013251112740016706 5ustar sborhosborho00000000000000tortoisehg-4.5.2/tortoisehg/util/0000755000175000017500000000000013251112740017663 5ustar sborhosborho00000000000000tortoisehg-4.5.2/tortoisehg/util/thread2.py0000644000175000017500000000350413150123225021566 0ustar sborhosborho00000000000000# 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-4.5.2/tortoisehg/util/wconfig.py0000644000175000017500000002171113242076403021700 0ustar sborhosborho00000000000000# 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 re 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 ?? from iniparse.ini import CommentLine # Monkypatch this regex to prevent iniparse from considering # 'rem' as a comment CommentLine.regex = re.compile(r'^(?P[%;#])(?P.*)$') # allow :suboption in from iniparse.ini import OptionLine OptionLine.regex = re.compile(r'^(?P[^:=\s[][^=]*)' r'(?P=\s*)' r'(?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): if isinstance(src, _wsortdict): src = src._dict 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]: # get around COW behavior introduced by hg c41444a39de2, where # an inner dict may be replaced later on preparewrite(). our # wrapper expects non-empty config[section] instance persists. data = self._config._data data[section] = data[section].preparewrite() 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""" # normalize line endings buf = cStringIO.StringIO() config.write(buf) data = '\n'.join(buf.getvalue().splitlines()) + '\n' if os.name == 'nt': # no atomic rename to the existing file that may fail occasionally # for unknown reasons, possibly because of our QFileSystemWatcher or # a virus scanner. also it breaks NTFS symlink (issue #2181). openfile = util.posixfile else: # atomic rename is reliable on Unix openfile = util.atomictempfile f = openfile(os.path.realpath(path), 'w') try: f.write(data) f.close() finally: del f # unlink temp file tortoisehg-4.5.2/tortoisehg/util/paths.py0000644000175000017500000001207413150123225021356 0ustar sborhosborho00000000000000# 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 tortoisehg.util.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, shlex 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: if 'HG' in os.environ: try: _hg_command = shlex.split(os.environ['HG'], posix=(os.name != 'nt')) except ValueError: _hg_command = [os.environ['HG']] else: _hg_command = _find_hg_command() return _hg_command if os.name == 'nt': 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): return os.path.dirname(sys.executable) return os.path.dirname(os.path.dirname(os.path.dirname(__file__))) def get_thg_command(): if getattr(sys, 'frozen', False): return [sys.executable] return [sys.executable] + sys.argv[:1] 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 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 get_thg_command(): return sys.argv[:1] def is_unc_path(path): return False def is_on_fixed_drive(path): return True tortoisehg-4.5.2/tortoisehg/util/colormap.py0000644000175000017500000001004613150123225022050 0ustar sborhosborho00000000000000# 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, isdarktheme=False): self._maxhues = maxhues self._maxsaturations = maxsaturations self._isdarktheme = isdarktheme 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): if self._isdarktheme: return int(saturation / 4 * (1 - hv)) else: 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, isdarktheme=False): """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, isdarktheme)[0] def _makeannotatepalette(sortedfctxs, now, maxcolors, maxhues, maxsaturations, mindate, isdarktheme): cm = AnnotateColorSaturation(maxhues=maxhues, maxsaturations=maxsaturations, isdarktheme=isdarktheme) 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, isdarktheme) # 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-4.5.2/tortoisehg/util/__version__.py0000644000175000017500000000007313251112740022516 0ustar sborhosborho00000000000000# this file is autogenerated by setup.py version = "4.5.2" tortoisehg-4.5.2/tortoisehg/util/hgdispatch.py0000644000175000017500000000457713150123225022366 0ustar sborhosborho00000000000000# hgdispatch.py - Mercurial command wrapper for TortoiseHg # # Copyright 2007, 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 or any later version. import urllib2, urllib from mercurial import error, extensions, subrepo, util from mercurial import dispatch as dispatchmod from tortoisehg.util import hgversion from tortoisehg.util.i18n import agettext as _ testedwith = hgversion.testedwith # exception handling different from _runcatch() def _dispatch(orig, req): ui = req.ui try: return orig(req) except subrepo.SubrepoAbort, e: errormsg = str(e) label = 'ui.error' if e.subrepo: label += ' subrepo=%s' % urllib.quote(e.subrepo) ui.write_err(_('abort: ') + errormsg + '\n', label=label) if e.hint: ui.write_err(_('hint: ') + str(e.hint) + '\n', label=label) except util.Abort, e: ui.write_err(_('abort: ') + str(e) + '\n', label='ui.error') if e.hint: ui.write_err(_('hint: ') + str(e.hint) + '\n', label='ui.error') except error.RepoError, e: ui.write_err(str(e) + '\n', label='ui.error') except urllib2.HTTPError, e: err = _('HTTP Error: %d (%s)') % (e.code, e.msg) ui.write_err(err + '\n', label='ui.error') except urllib2.URLError, e: err = _('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 = _('SSL: Server certificate verify failed') elif _errno == '00000000': err = _('SSL: unknown error %s:%s') % (file, line) else: err = _('SSL error: %s') % reason except ImportError: pass ui.write_err(err + '\n', label='ui.error') return -1 def uisetup(ui): # uisetup() is called after the initial dispatch(), so this only makes an # effect on command server extensions.wrapfunction(dispatchmod, '_dispatch', _dispatch) tortoisehg-4.5.2/tortoisehg/util/win32ill.py0000644000175000017500000002066413150123225021706 0ustar sborhosborho00000000000000# win32ill.py - listen to WM_CLOSE to shutdown cleanly # # Copyright 2014 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. """listen to WM_CLOSE to shutdown cleanly In short, this extension provides alternative to `kill pid` on Windows. Background: Windows cannot send Ctrl-C signal to CLI process without a console window, which means there is no easy way to abort `hg push` safely from your application. `GenerateConsoleCtrlEvent()` is very attractive, but look now, it's just able to signal `CTRL_C_EVENT` to all processes sharing the console. This extension spawns a thread to listen to `WM_CLOSE` message, and generates `CTRL_C_EVENT` to itself on `WM_CLOSE`. - http://stackoverflow.com/questions/1453520/ - http://msdn.microsoft.com/en-us/library/windows/desktop/ms683155(v=vs.85).aspx - http://support.microsoft.com/kb/178893/en-us Caveats: - Make sure to set `CREATE_NO_WINDOW` or `CREATE_NEW_CONSOLE` to `dwCreationFlags` when creating hg process; otherwise the master process will also receive `CTRL_C_EVENT`. - If the master process communicates with the sub hg process via stdio, the master also needs to close the write channel of the sub. - Blocking winsock calls cannot be interrupted as Ctrl-C in `cmd.exe` has no effect. """ import atexit, ctypes, os, threading from mercurial import util from tortoisehg.util import hgversion from tortoisehg.util.i18n import agettext as _ testedwith = hgversion.testedwith _CTRL_C_EVENT = 0 _WM_APP = 0x8000 _WM_CLOSE = 0x0010 _WM_DESTROY = 0x0002 _WS_EX_NOACTIVATE = 0x08000000 _WS_POPUP = 0x80000000 _WM_STOPMESSAGELOOP = _WM_APP + 0 def _errcheckbool(result, func, args): if not result: raise ctypes.WinError() return args def _errcheckminus1(result, func, args): if result == -1: raise ctypes.WinError() return args if os.name == 'nt': from ctypes import wintypes _ATOM = wintypes.ATOM _BOOL = wintypes.BOOL _DWORD = wintypes.DWORD _HBRUSH = wintypes.HBRUSH _HCURSOR = wintypes.HICON _HICON = wintypes.HICON _HINSTANCE = wintypes.HINSTANCE _HMENU = wintypes.HMENU _HMODULE = wintypes.HMODULE _HWND = wintypes.HWND _LPARAM = wintypes.LPARAM _LPCTSTR = wintypes.LPCSTR _LPVOID = wintypes.LPVOID _LRESULT = wintypes.LPARAM # LRESULT and LPARAM are defined as LONG_PTR _MSG = wintypes.MSG _UINT = wintypes.UINT _WPARAM = wintypes.WPARAM _WNDPROC = ctypes.WINFUNCTYPE(_LRESULT, _HWND, _UINT, _WPARAM, _LPARAM) class _WNDCLASS(ctypes.Structure): _fields_ = [ ('style', _UINT), ('lpfnWndProc', _WNDPROC), ('cbClsExtra', ctypes.c_int), ('cbWndExtra', ctypes.c_int), ('hInstance', _HINSTANCE), ('hIcon', _HICON), ('hCursor', _HCURSOR), ('hbrBackground', _HBRUSH), ('lpszMenuName', _LPCTSTR), ('lpszClassName', _LPCTSTR), ] _CreateWindowEx = ctypes.windll.user32.CreateWindowExA _CreateWindowEx.restype = _HWND _CreateWindowEx.argtypes = (_DWORD, _LPCTSTR, _LPCTSTR, _DWORD, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, _HWND, _HMENU, _HINSTANCE, _LPVOID) _CreateWindowEx.errcheck = _errcheckbool _DefWindowProc = ctypes.windll.user32.DefWindowProcA _DefWindowProc.restype = _LRESULT _DefWindowProc.argtypes = (_HWND, _UINT, _WPARAM, _LPARAM) _DestroyWindow = ctypes.windll.user32.DestroyWindow _DestroyWindow.restype = _BOOL _DestroyWindow.argtypes = (_HWND,) _DestroyWindow.errcheck = _errcheckbool _DispatchMessage = ctypes.windll.user32.DispatchMessageA _DispatchMessage.restype = _LRESULT _DispatchMessage.argtypes = (ctypes.POINTER(_MSG),) _GenerateConsoleCtrlEvent = ctypes.windll.kernel32.GenerateConsoleCtrlEvent _GenerateConsoleCtrlEvent.restype = _BOOL _GenerateConsoleCtrlEvent.argtypes = (_DWORD, _DWORD) _GenerateConsoleCtrlEvent.errcheck = _errcheckbool _GetMessage = ctypes.windll.user32.GetMessageA _GetMessage.restype = _BOOL # -1, 0, or non-zero _GetMessage.argtypes = (ctypes.POINTER(_MSG), _HWND, _UINT, _UINT) _GetMessage.errcheck = _errcheckminus1 _GetModuleHandle = ctypes.windll.kernel32.GetModuleHandleA _GetModuleHandle.restype = _HMODULE _GetModuleHandle.argtypes = (_LPCTSTR,) _GetModuleHandle.errcheck = _errcheckbool _PostQuitMessage = ctypes.windll.user32.PostQuitMessage _PostQuitMessage.restype = None _PostQuitMessage.argtypes = (ctypes.c_int,) _PostMessage = ctypes.windll.user32.PostMessageA _PostMessage.restype = _BOOL _PostMessage.argtypes = (_HWND, _UINT, _WPARAM, _LPARAM) _PostMessage.errcheck = _errcheckbool _RegisterClass = ctypes.windll.user32.RegisterClassA _RegisterClass.restype = _ATOM _RegisterClass.argtypes = (ctypes.POINTER(_WNDCLASS),) _RegisterClass.errcheck = _errcheckbool _TranslateMessage = ctypes.windll.user32.TranslateMessage _TranslateMessage.restype = _BOOL _TranslateMessage.argtypes = (ctypes.POINTER(_MSG),) class messageserver(object): def __init__(self, logfile): self._logfile = logfile self._thread = threading.Thread(target=self._mainloop) self._thread.setDaemon(True) # skip global join before atexit self._wndcreated = threading.Event() self._hwnd = None self._wndclass = wc = _WNDCLASS() wc.lpfnWndProc = _WNDPROC(self._wndproc) wc.hInstance = _GetModuleHandle(None) wc.lpszClassName = 'HgMessage' _RegisterClass(ctypes.byref(wc)) def start(self): if self._hwnd: raise RuntimeError('window already created') self._wndcreated.clear() self._thread.start() self._wndcreated.wait() if not self._hwnd: raise util.Abort(_('win32ill: cannot create window for messages')) def stop(self): hwnd = self._hwnd if hwnd: _PostMessage(hwnd, _WM_STOPMESSAGELOOP, 0, 0) self._thread.join() def _log(self, msg): if not self._logfile: return self._logfile.write(msg + '\n') self._logfile.flush() def _mainloop(self): try: # no HWND_MESSAGE so that it can be found by EnumWindows # WS_EX_NOACTIVATE and WS_POPUP exist just for strictness self._hwnd = _CreateWindowEx( _WS_EX_NOACTIVATE, # dwExStyle self._wndclass.lpszClassName, None, # lpWindowName _WS_POPUP, # dwStyle 0, 0, 0, 0, # x, y, nWidth, nHeight None, # hWndParent None, # hMenu _GetModuleHandle(None), # hInstance None) # lpParam finally: self._wndcreated.set() self._log('starting message loop (pid = %d)' % os.getpid()) msg = _MSG() lpmsg = ctypes.byref(msg) while _GetMessage(lpmsg, None, 0, 0): _TranslateMessage(lpmsg) _DispatchMessage(lpmsg) def _wndproc(self, hwnd, msg, wparam, lparam): if msg == _WM_CLOSE: self._log('received WM_CLOSE') # dwProcessGroupId=0 means all processes sharing the same console, # which is the only choice for CTRL_C_EVENT. _GenerateConsoleCtrlEvent(_CTRL_C_EVENT, 0) return 0 if msg == _WM_STOPMESSAGELOOP and self._hwnd: self._log('destroying window') _DestroyWindow(self._hwnd) self._hwnd = None if msg == _WM_DESTROY: self._log('received WM_DESTROY') _PostQuitMessage(0) return 0 return _DefWindowProc(hwnd, msg, wparam, lparam) def _openlogfile(ui): log = ui.config('win32ill', 'log') if log == '-': return ui.ferr elif log: return open(log, 'a') def uisetup(ui): if os.name != 'nt': ui.warn(_('win32ill: unsupported platform: %s\n') % os.name) return # message loop is per process sv = messageserver(_openlogfile(ui)) def stop(): try: sv.stop() except KeyboardInterrupt: # can happen if command finished just before WM_CLOSE request ui.warn(_('win32ill: interrupted while stopping message loop\n')) atexit.register(stop) sv.start() tortoisehg-4.5.2/tortoisehg/util/debugthg.py0000644000175000017500000000272113150123225022026 0ustar sborhosborho00000000000000# 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-4.5.2/tortoisehg/util/patchctx.py0000644000175000017500000001742113242076403022065 0ustar sborhosborho00000000000000# 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 binascii import cStringIO from mercurial import patch, util, error from mercurial import node from mercurial.util import propertycache from hgext import mq 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 if rev: assert isinstance(rev, str) self._patchname = rev else: 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._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 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 instabilities(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 thgmqappliedpatch(self): return False def thgmqpatchname(self): return self._patchname def thgmqunappliedpatch(self): return True # largefiles/kbfiles methods def hasStandin(self, file): return False def isStandin(self, path): return False 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''' if not util.safehasattr(self, '_ph'): return '' 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: # consume comments and headers for i in range(self._ph.diffstartline): pf.readline() for chunk in patch.parsepatch(pf): if not isinstance(chunk, patch.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-4.5.2/tortoisehg/util/menuthg.py0000644000175000017500000002646213150123225021714 0ustar sborhosborho00000000000000# 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, node, error from tortoisehg.util.i18n import _ as gettext from tortoisehg.util import cachethg, paths, hglib def _(msgid): return {'id': msgid, 'str': gettext(msgid).encode('utf-8')} 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(hglib.loadui(), 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 = hglib.loadui().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(hglib.loadui(), 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-4.5.2/tortoisehg/util/hgcommands.py0000644000175000017500000001111413242076403022360 0ustar sborhosborho00000000000000# hgcommands.py - miscellaneous Mercurial commands for TortoiseHg # # Copyright 2013, 2014 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. from __future__ import absolute_import import hashlib import os import socket from mercurial import ( extensions, registrar, sslutil, util, ) from tortoisehg.util import ( hgversion, ) from tortoisehg.util.i18n import agettext as _ cmdtable = {} _mqcmdtable = {} command = registrar.command(cmdtable) mqcommand = registrar.command(_mqcmdtable) configtable = {} configitem = registrar.configitem(configtable) testedwith = hgversion.testedwith configitem('tortoisehg', 'initskel') @command('debuggethostfingerprint', [('', 'insecure', None, _('do not verify server certificate (ignoring web.cacerts config)')), ], _('[--insecure] [SOURCE]'), optionalrepo=True) def debuggethostfingerprint(ui, repo, source='default', **opts): """retrieve a fingerprint of the server certificate Specify --insecure to disable SSL verification. """ source = ui.expandpath(source) u = util.url(source) scheme = (u.scheme or '').split('+')[-1] host = u.host port = util.getport(u.port or scheme or '-1') if scheme != 'https' or not host or not (0 <= port <= 65535): raise util.Abort(_('unsupported URL: %s') % source) sock = socket.socket() try: sock.connect((host, port)) sock = sslutil.wrapsocket(sock, None, None, ui, serverhostname=host) peercert = sock.getpeercert(True) if not peercert: raise util.Abort(_('%s certificate error: no certificate received') % host) finally: sock.close() s = hashlib.sha256(peercert).hexdigest() ui.write('sha256:', ':'.join([s[x:x + 2] for x in xrange(0, len(s), 2)]), '\n') def postinitskel(ui, repo, hooktype, result, pats, **kwargs): """create common files in new repository""" assert hooktype == 'post-init' if result: return dest = ui.expandpath(pats and pats[0] or '.') skel = ui.config('tortoisehg', 'initskel') if skel: # copy working tree from user-defined path if any skel = util.expandpath(skel) for name in os.listdir(skel): if name == '.hg': continue util.copyfiles(os.path.join(skel, name), os.path.join(dest, name), hardlink=False) return # create .hg* files, mainly to workaround Explorer's problem in creating # files with a name beginning with a dot open(os.path.join(dest, '.hgignore'), 'a').close() 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 @mqcommand('qreorder', [('', 'after', '', _('move after the specified patch'))], _('[--after PATCH] PATCH...')) def qreorder(ui, repo, *patches, **opts): """move patches to the beginning or after the specified patch""" after = opts['after'] or None q = repo.mq if any(n not in q.series for n in patches): raise util.Abort(_('unknown patch to move specified')) if after in patches: raise util.Abort(_('invalid patch position specified')) if any(q.isapplied(n) for n in patches): raise util.Abort(_('cannot move applied patches')) if after is None: at = 0 else: try: at = q.series.index(after) + 1 except ValueError: raise util.Abort(_('patch %s not in series') % after) if at < q.seriesend(True): raise util.Abort(_('cannot move into applied patches')) wlock = repo.wlock() try: _applymovemqpatches(q, after, patches) q.savedirty() finally: wlock.release() def uisetup(ui): try: extensions.find('mq') cmdtable.update(_mqcmdtable) except KeyError: pass tortoisehg-4.5.2/tortoisehg/util/partialcommit.py0000644000175000017500000000637713150123225023115 0ustar sborhosborho00000000000000# 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 from tortoisehg.util import hgversion testedwith = hgversion.testedwith 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: # patch files in tmp directory patch.patchrepo(ui, repo, repo['.'], store, fp, 1, prefix='') 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() del repo._filestore 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) def uisetup(ui): 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-4.5.2/tortoisehg/util/obsoleteutil.py0000644000175000017500000000541413251112734022756 0ustar sborhosborho00000000000000# 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. from mercurial import error def precursorsmarkers(obsstore, node): return obsstore.precursors.get(node, ()) def successorsmarkers(obsstore, node): return obsstore.successors.get(node, ()) def first_known_precursors_rev(repo, rev): if rev is None or not isinstance(rev, int): return obsstore = getattr(repo, 'obsstore', None) if not obsstore: return clog = repo.changelog nm = clog.nodemap start = clog.node(rev) markers = precursorsmarkers(obsstore, start) candidates = set(mark[0] for mark in markers) seen = set(candidates) if start in candidates: candidates.remove(start) else: seen.add(start) while candidates: current = candidates.pop() crev = nm.get(current) if crev is not None: try: repo[crev] # filter out filtered revisions yield crev continue except error.RepoLookupError: pass for mark in precursorsmarkers(obsstore, current): if mark[0] not in seen: candidates.add(mark[0]) seen.add(mark[0]) def first_known_precursors(ctx): for rev in first_known_precursors_rev(ctx._repo, ctx.rev()): yield ctx._repo[rev] def first_known_successors(ctx): 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: try: yield ctx._repo[crev] continue except error.RepoLookupError: # filtered-out changeset pass for mark in successorsmarkers(obsstore, current): for succ in mark[1]: if succ not in seen: candidates.add(succ) seen.add(succ) tortoisehg-4.5.2/tortoisehg/util/version.py0000644000175000017500000000465613150123225021733 0ustar sborhosborho00000000000000# 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 _ as _gettext # TODO: use unicode version globally def _(message, context=''): return _gettext(message, context).encode('utf-8') 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() # prevent loading additional extensions for k, _v in u.configitems('extensions'): u.setconfig('extensions', k, '!') 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().rpartition(':')[2] + 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-4.5.2/tortoisehg/util/hglib.py0000644000175000017500000010640013251112734021326 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import cStringIO import glob import os import re import shlex import sys import time from hgext import mq as mqmod from mercurial import ( dispatch as dispatchmod, encoding, error, extensions, fancyopts, filemerge, fileset, mdiff, merge as mergemod, pathutil, rcutil, revset as revsetmod, revsetlang, templatefilters, ui as uimod, util, ) from tortoisehg.util import paths from mercurial.node import nullrev from tortoisehg.util.hgversion import hgversion from tortoisehg.util.i18n import ( _ as _gettext, ngettext as _ngettext, ) _encoding = encoding.encoding _fallbackencoding = encoding.fallbackencoding # extensions which can cause problem with TortoiseHg _extensions_blacklist = ( 'blackbox', # mucks uimod.ui (hg 851c41a21869, issue #4489) 'color', 'pager', 'progress', 'zeroconf', ) tokenizerevspec = revsetlang.tokenize userrcpath = rcutil.userrcpath # TODO: use unicode version globally def _(message, context=''): return _gettext(message, context).encode('utf-8') def ngettext(singular, plural, n): return _ngettext(singular, plural, n).encode('utf-8') 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') try: return s.decode(_encoding, '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 activebookmark(repo): return repo._activebookmark def namedbranches(repo): branchmap = repo.branchmap() dead = repo.deadbranches return sorted(br for br, _heads, _tip, isclosed in branchmap.iterbranches() if not isclosed and br not in dead) def _firstchangectx(repo): try: # try fast path, which may be hidden return repo[0] except error.RepoLookupError: pass for rev in revsetmod.spanset(repo): return repo[rev] return repo[nullrev] def shortrepoid(repo): """Short hash of the first root changeset; can be used for settings key""" return str(_firstchangectx(repo)) def repoidnode(repo): """Hash of the first root changeset in binary form""" return _firstchangectx(repo).node() def _getfirstrevisionlabel(repo, ctx): # see context.changectx for look-up order of labels bookmarks = ctx.bookmarks() if ctx in repo[None].parents(): # keep bookmark unchanged when updating to current rev if activebookmark(repo) in bookmarks: return activebookmark(repo) 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) 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 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 = tounicode(ui.popbuffer()).splitlines() except (util.Abort, EnvironmentError): qqueues = [] return qqueues readmergestate = mergemod.mergestate.read def readundodesc(repo): """Read short description and changelog size of last transaction""" if os.path.exists(repo.sjoin('undo')): try: args = repo.vfs('undo.desc', 'r').read().splitlines() return args[1], int(args[0]) except (IOError, IndexError, ValueError): pass return '', len(repo) def unidifftext(a, ad, b, bd, fn1, fn2, opts=mdiff.defaultopts): headers, hunks = mdiff.unidiff(a, ad, b, bd, fn1, fn2, opts) text = ''.join(sum((list(hlines) for _hrange, hlines in hunks), [])) return '\n'.join(headers) + '\n' + text 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) if hasattr(sys, "frozen"): if 'hgsubversion' not in exts: exts['hgsubversion'] = _('hgsubversion packaged with thg') if 'hggit' not in exts: exts['hggit'] = _('hggit packaged with thg') 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 _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 loadextensions(ui): """Load and setup extensions for GUI process""" _wrapextensionsloader() # enable blacklist of extensions extensions.loadall(ui) # TODO: provide singular canonpath() wrapper instead? 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(pathutil.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( pathutil.canonpath(root, cwd, f[len(cwd + os.sep):])) else: # May already be canonical canonpats.append(f) return canonpats def normreporoot(path): """Normalize repo root path in the same manner as localrepository""" # see localrepo.localrepository and scmutil.vfs lpath = fromunicode(path) lpath = os.path.realpath(util.expandpath(lpath)) return tounicode(lpath) 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.pairselection.custom-menu', _('Pair selection context menu')), ('workbench.multipleselection.custom-menu', _('Multiple selection 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(uimod.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, uimod.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('.', 1) 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 loadui = uimod.ui.load def copydynamicconfig(srcui, destui): """Copy config values that come from command line or code >>> srcui = uimod.ui() >>> srcui.setconfig('paths', 'default', 'http://example.org/', ... '/repo/.hg/hgrc:2') >>> srcui.setconfig('patch', 'eol', 'auto', 'eol') >>> destui = uimod.ui() >>> copydynamicconfig(srcui, destui) >>> destui.config('paths', 'default') is None True >>> destui.config('patch', 'eol'), destui.configsource('patch', 'eol') ('auto', 'eol') """ for section, name, value in srcui.walkconfig(): source = srcui.configsource(section, name) if ':' in source: # path:line continue if source == 'none': # ui.configsource returns 'none' by default source = '' destui.setconfig(section, name, value, source) def shortreponame(ui): name = ui.config('web', 'name', None) if not name: return src = ui.configsource('web', 'name') # path:line if '/.hg/hgrc:' not in util.pconvert(src): # global web.name will set the same name to all repositories ui.debug('ignoring global web.name defined at %s\n' % src) return return name def extractchoices(prompttext): """Extract prompt message and list of choice (char, label) pairs This is slightly different from ui.extractchoices() in that a. prompttext may be a unicode b. choice label includes &-accessor >>> extractchoices("awake? $$ &Yes $$ &No") ('awake? ', [('y', '&Yes'), ('n', '&No')]) >>> extractchoices("line\\nbreak? $$ &Yes $$ &No") ('line\\nbreak? ', [('y', '&Yes'), ('n', '&No')]) >>> extractchoices("want lots of $$money$$?$$Ye&s$$N&o") ('want lots of $$money$$?', [('s', 'Ye&s'), ('o', 'N&o')]) """ m = re.match(r'(?s)(.+?)\$\$([^\$]*&[^ \$].*)', prompttext) msg = m.group(1) choices = [p.strip(' ') for p in m.group(2).split('$$')] resps = [p[p.index('&') + 1].lower() for p in choices] return msg, zip(resps, choices) 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 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 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 = age(fctx.date()).decode('utf-8') 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 parseconfigopts(ui, args): """Pop the --config options from the command line and apply them >>> u = uimod.ui() >>> args = ['log', '--config', 'extensions.mq=!'] >>> parseconfigopts(u, args) [('extensions', 'mq', '!')] >>> args ['log'] >>> u.config('extensions', 'mq') '!' """ try: config = dispatchmod._earlyparseopts(ui, args)['config'] # drop --config from args args[:] = fancyopts.earlygetopt(args, '', ['config='], gnu=True, keepsep=True)[1] except (AttributeError, TypeError): # hg<4.5 (6e6d0a5b88e6) config = dispatchmod._earlygetopt(['--config'], args) return dispatchmod._parseconfig(ui, config) # (unicode, QString) -> unicode, otherwise -> str _stringify = '%s'.__mod__ # ASCII code -> escape sequence (see PyString_Repr()) _escapecharmap = [] _escapecharmap.extend('\\x%02x' % x for x in xrange(32)) _escapecharmap.extend(chr(x) for x in xrange(32, 127)) _escapecharmap.append('\\x7f') _escapecharmap[0x09] = '\\t' _escapecharmap[0x0a] = '\\n' _escapecharmap[0x0d] = '\\r' _escapecharmap[0x27] = "\\'" _escapecharmap[0x5c] = '\\\\' _escapecharre = re.compile(r'[\x00-\x1f\x7f\'\\]') def _escapecharrepl(m): return _escapecharmap[ord(m.group(0))] def escapeascii(s): r"""Escape string to be embedded as a literal; like Python string_escape, but keeps 8bit characters and can process unicode >>> escapeascii("\0 \x0b \x7f \t \n \r ' \\") "\\x00 \\x0b \\x7f \\t \\n \\r \\' \\\\" >>> escapeascii(u'\xc0\n') u'\xc0\\n' """ s = _stringify(s) return _escapecharre.sub(_escapecharrepl, s) def escapepath(path): r"""Convert path to command-line-safe string; path must be relative to the repository root >>> escapepath('foo/[bar].txt') 'path:foo/[bar].txt' >>> escapepath(u'\xc0') u'\xc0' """ p = _stringify(path) if '[' in p or '{' in p or '*' in p or '?' in p: # bare path is expanded by scmutil.expandpats() on Windows return 'path:' + p else: return p def escaperev(rev, default=None): """Convert revision number to command-line-safe string""" if rev is None: return default if rev == nullrev: return 'null' assert rev >= 0 return '%d' % rev def _escaperevrange(a, b): if a == b: return escaperev(a) else: return '%s:%s' % (escaperev(a), escaperev(b)) def compactrevs(revs): """Build command-line-safe revspec from list of revision numbers; revs should be sorted in ascending order to get compact form >>> compactrevs([]) '' >>> compactrevs([0]) '0' >>> compactrevs([0, 1]) '0:1' >>> compactrevs([-1, 0, 1, 3]) 'null:1 + 3' >>> compactrevs([0, 4, 5, 6, 8, 9]) '0 + 4:6 + 8:9' """ if not revs: return '' specs = [] k = m = revs[0] for n in revs[1:]: if m + 1 == n: m = n else: specs.append(_escaperevrange(k, m)) k = m = n specs.append(_escaperevrange(k, m)) return ' + '.join(specs) # subset of revsetlang.formatspec(), but can process unicode def _formatspec(expr, args, lparse, listfuncs): def argtype(c, arg): if c == 'd': return '%d' % int(arg) elif c == 's': return "'%s'" % escapeascii(arg) elif c == 'r': s = _stringify(arg) if isinstance(s, unicode): # 8-bit characters aren't important; just avoid encoding error s = s.encode('utf-8') lparse(s) # make sure syntax errors are confined return '(%s)' % arg raise ValueError('invalid format character %c' % c) def listexp(c, arg): l = len(arg) if l == 0: if 's' not in listfuncs: raise ValueError('cannot process empty list') return "%s('')" % listfuncs['s'] elif l == 1: return argtype(c, arg[0]) elif c in listfuncs: f = listfuncs[c] a = '\0'.join(map(_stringify, arg)) # packed argument is escaped so it is command-line safe return "%s('%s')" % (f, escapeascii(a)) m = l // 2 return '(%s or %s)' % (listexp(c, arg[:m]), listexp(c, arg[m:])) expr = _stringify(expr) argiter = iter(args) ret = [] pos = 0 while pos < len(expr): q = expr.find('%', pos) if q < 0: ret.append(expr[pos:]) break ret.append(expr[pos:q]) pos = q + 1 c = expr[pos] if c == '%': ret.append(c) elif c == 'l': pos += 1 d = expr[pos] ret.append(listexp(d, list(next(argiter)))) else: ret.append(argtype(c, next(argiter))) pos += 1 return ''.join(ret) def formatfilespec(expr, *args): """Build fileset expression by template and positional arguments Supported arguments: %r = fileset expression, parenthesized %d = int(arg), no quoting %s = string(arg), escaped and single-quoted %% = a literal '%' Prefixing the type with 'l' specifies a parenthesized list of that type, but the list must not be empty. """ listfuncs = {} return _formatspec(expr, args, fileset.parse, listfuncs) def formatrevspec(expr, *args): r"""Build revset expression by template and positional arguments Supported arguments: %r = revset expression, parenthesized %d = int(arg), no quoting %s = string(arg), escaped and single-quoted %% = a literal '%' Prefixing the type with 'l' specifies a parenthesized list of that type. >>> formatrevspec('%r:: and %lr', u'10 or "\xe9"', ("this()", "that()")) u'(10 or "\xe9"):: and ((this()) or (that()))' >>> formatrevspec('%d:: and not %d::', 10, 20) '10:: and not 20::' >>> formatrevspec('%ld or %ld', [], [1]) "_list('') or 1" >>> formatrevspec('keyword(%s)', u'foo\xe9') u"keyword('foo\xe9')" >>> formatrevspec('root(%ls)', ['a', 'b', 'c', 'd']) "root(_list('a\\x00b\\x00c\\x00d'))" """ listfuncs = {'d': '_intlist', 's': '_list'} return _formatspec(expr, args, revsetlang.parse, listfuncs) 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', '-r0', '-r1'] >>> buildcmdargs('diff', r=[0, None]) ['diff', '-r0'] >>> 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: >>> buildcmdargs('email', r=[0, 1]) ['email', '-r0', '-r1'] >>> buildcmdargs('grep', 'foo', rev=2) ['grep', '--rev=2', 'foo'] >>> buildcmdargs('tag', u'\xc0', message=u'\xc1') ['tag', u'--message=\xc1', u'\xc0'] """ fullargs = [_stringify(name)] for k, v in opts.iteritems(): if v is None: continue if len(k) == 1: aname = '-%s' % k apref = aname else: aname = '--%s' % k.replace('_', '-') apref = aname + '=' if isinstance(v, bool): if v: fullargs.append(aname) elif isinstance(v, list): for e in v: if e is None: continue fullargs.append(apref + _stringify(e)) else: fullargs.append(apref + _stringify(v)) args = [_stringify(v) for v in args if v is not None] if any(e.startswith('-') for e in args): fullargs.append('--') fullargs.extend(args) return fullargs _urlpassre = re.compile(r'^([a-zA-Z0-9+.\-]+://[^:@/]*):[^@/]+@') def _reprcmdarg(arg): arg = _urlpassre.sub(r'\1:***@', arg) arg = arg.replace('\n', '^M') # 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(['log', 'foo\\bar', '', 'foo bar', 'foo"bar']) 'log "foo\\bar" "" "foo bar" "foo\\"bar"' >>> prettifycmdline(['log', '--template', '{node}\n']) 'log --template {node}^M' mask password in url-like string: >>> prettifycmdline(['push', 'http://foo123:bar456@example.org/']) 'push http://foo123:***@example.org/' >>> prettifycmdline(['clone', 'svn+http://:bar@example.org:8080/trunk/']) 'clone svn+http://:***@example.org:8080/trunk/' """ return ' '.join(_reprcmdarg(e) for e in cmdline) def parsecmdline(cmdline, cwd): r"""Split command line string to imitate a unix shell >>> origfuncs = glob.glob, os.path.expanduser, os.path.expandvars >>> glob.glob = lambda p: [p.replace('*', e) for e in ['foo', 'bar', 'baz']] >>> os.path.expanduser = lambda p: re.sub(r'^~', '/home/foo', p) >>> os.path.expandvars = lambda p: p.replace('$var', 'bar') emulates glob/variable expansion rule for simple cases: >>> parsecmdline('foo * "qux quux" "*" "*"', '.') [u'foo', u'foo', u'bar', u'baz', u'qux quux', u'*', u'*'] >>> parsecmdline('foo /*', '.') [u'foo', u'/foo', u'/bar', u'/baz'] >>> parsecmdline('''foo ~/bar '~/bar' "~/bar"''', '.') [u'foo', u'/home/foo/bar', u'~/bar', u'~/bar'] >>> parsecmdline('''foo $var '$var' "$var"''', '.') [u'foo', u'bar', u'$var', u'bar'] but the following cases are unsupported: >>> parsecmdline('"foo"*"bar"', '.') # '*' should be expanded [u'foo*bar'] >>> parsecmdline(r'\*', '.') # '*' should be a literal [u'foo', u'bar', u'baz'] >>> glob.glob, os.path.expanduser, os.path.expandvars = origfuncs """ _ = _gettext # TODO: use unicode version globally # shlex can't process unicode on Python < 2.7.3 cmdline = cmdline.encode('utf-8') src = cStringIO.StringIO(cmdline) lex = shlex.shlex(src, posix=True) lex.whitespace_split = True lex.commenters = '' args = [] while True: # peek first char of next token to guess its type. this isn't perfect # but can catch common cases. q = cmdline[src.tell():].lstrip(lex.whitespace)[:1] try: e = lex.get_token() except ValueError as err: raise ValueError(_('command parse error: %s') % err) if e == lex.eof: return args e = e.decode('utf-8') if q not in lex.quotes or q in lex.escapedquotes: e = os.path.expandvars(e) # $var or "$var" if q not in lex.quotes: e = os.path.expanduser(e) # ~user if q not in lex.quotes and any(c in e for c in '*?[]'): expanded = glob.glob(os.path.join(cwd, e)) if not expanded: raise ValueError(_('no matches found: %s') % e) if os.path.isabs(e): args.extend(expanded) else: args.extend(p[len(cwd) + 1:] for p in expanded) else: args.append(e) tortoisehg-4.5.2/tortoisehg/util/editor.py0000644000175000017500000000736113205035322021531 0ustar sborhosborho00000000000000import 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 = ui.geteditor() 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-4.5.2/tortoisehg/util/hgversion.py0000644000175000017500000000175313242076403022254 0ustar sborhosborho00000000000000# 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() testedwith = '4.4 4.5' def checkhgversion(v): """range check the Mercurial version""" reqvers = testedwith.split() 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 len(vers) < 2: return if '.'.join(vers) in reqvers: return return ('This version of TortoiseHg requires Mercurial version %s.n to ' '%s.n, but found %s') % (reqvers[0], reqvers[-1], v) tortoisehg-4.5.2/tortoisehg/util/configitems.py0000644000175000017500000001121113242076403022545 0ustar sborhosborho00000000000000# configitems.py - declaration of TortoiseHg configurations # # Copyright 2018 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. from __future__ import absolute_import from mercurial import ( registrar, ) from tortoisehg.util import ( hgversion, ) configtable = {} configitem = registrar.configitem(configtable) testedwith = hgversion.testedwith configitem('experimental', 'graph-group-branches', default=False) configitem('experimental', 'graph-group-branches.firstbranch', default='') configitem('experimental', 'thg.displaynames', default=None) configitem('reviewboard', 'browser', default=None) configitem('reviewboard', 'password', default=None) configitem('reviewboard', 'repoid', default=None) configitem('reviewboard', 'server', default=None) configitem('reviewboard', 'user', default=None) configitem('tortoisehg', 'activatebookmarks', default='prompt') configitem('tortoisehg', 'authorcolor', default=False) configitem('tortoisehg', 'autoinc', default='') configitem('tortoisehg', 'autoresolve', default=True) configitem('tortoisehg', 'branchcolors', default=None) configitem('tortoisehg', 'changeset.link', default=None) configitem('tortoisehg', 'ciexclude', default='') configitem('tortoisehg', 'cipushafter', default=None) configitem('tortoisehg', 'closeci', default=False) configitem('tortoisehg', 'cmdserver.readtimeout', default=30) configitem('tortoisehg', 'confirmaddfiles', default=True) configitem('tortoisehg', 'confirmdeletefiles', default=True) configitem('tortoisehg', 'confirmpush', default=True) configitem('tortoisehg', 'deadbranch', default='') configitem('tortoisehg', 'defaultpush', default='all') configitem('tortoisehg', 'defaultwidget', default=None) configitem('tortoisehg', 'editor', default=None) configitem('tortoisehg', 'engmsg', default=False) configitem('tortoisehg', 'fontcomment', default=configitem.dynamicdefault) configitem('tortoisehg', 'fontdiff', default=configitem.dynamicdefault) configitem('tortoisehg', 'fontlog', default=configitem.dynamicdefault) configitem('tortoisehg', 'fontoutputlog', default=configitem.dynamicdefault) configitem('tortoisehg', 'forcerepotab', default=False) configitem('tortoisehg', 'forcevdiffwin', default=False) configitem('tortoisehg', 'fullauthorname', default=False) configitem('tortoisehg', 'fullpath', default=False) configitem('tortoisehg', 'graphlimit', default=500) configitem('tortoisehg', 'graphopt', default=False) configitem('tortoisehg', 'guifork', default=None) configitem('tortoisehg', 'hidetags', default='') configitem('tortoisehg', 'immediate', default='') configitem('tortoisehg', 'initialrevision', default='current') configitem('tortoisehg', 'initskel', default=None) configitem('tortoisehg', 'issue.bugtraqparameters', default=None) configitem('tortoisehg', 'issue.bugtraqplugin', default=None) configitem('tortoisehg', 'issue.bugtraqtrigger', default=None) configitem('tortoisehg', 'issue.inlinetags', default=False) configitem('tortoisehg', 'issue.link', default=None) configitem('tortoisehg', 'issue.linkmandatory', default=False) configitem('tortoisehg', 'issue.regex', default=None) configitem('tortoisehg', 'longsummary', default=False) configitem('tortoisehg', 'maxdiff', default=None) configitem('tortoisehg', 'monitorrepo', default='localonly') configitem('tortoisehg', 'opentabsaftercurrent', default=True) configitem('tortoisehg', 'postpull', default=None) configitem('tortoisehg', 'promoteditems', default='commit,log') configitem('tortoisehg', 'readme', default=None) configitem('tortoisehg', 'recurseinsubrepos', default=None) configitem('tortoisehg', 'refreshwdstatus', default='auto') configitem('tortoisehg', 'shell', default=None) configitem('tortoisehg', 'showfamilyline', default=True) configitem('tortoisehg', 'summarylen', default=None) configitem('tortoisehg', 'tabwidth', default=None) configitem('tortoisehg', 'tasktabs', default='off') configitem('tortoisehg', 'ui.language', default=None) configitem('tortoisehg', 'vdiff', default=None) configitem('tortoisehg', 'workbench.commit.custom-menu', default=list) configitem('tortoisehg', 'workbench.custom-toolbar', default=list) configitem('tortoisehg', 'workbench.filelist.custom-menu', default=list) configitem('tortoisehg', 'workbench.multipleselection.custom-menu', default=list) configitem('tortoisehg', 'workbench.pairselection.custom-menu', default=list) configitem('tortoisehg', 'workbench.revdetails.custom-menu', default=list) configitem('tortoisehg', 'workbench.single', default=True) configitem('tortoisehg', 'workbench.target-combo', default='auto') configitem('tortoisehg', 'workbench.task-toolbar', default=list) tortoisehg-4.5.2/tortoisehg/util/cachethg.py0000644000175000017500000001704313150123225022006 0ustar sborhosborho00000000000000# 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, node, 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 not paths.is_on_fixed_drive(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(hglib.loadui(), 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 # 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 = hglib.readmergestate(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-4.5.2/tortoisehg/util/thgstatus.py0000644000175000017500000000316313150123225022264 0ustar sborhosborho00000000000000# 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.vfs.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-4.5.2/tortoisehg/util/i18n.py0000644000175000017500000000550413150123225021016 0ustar sborhosborho00000000000000# 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 tortoisehg.util import paths _localeenvs = ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG') def _defaultlanguage(): if os.name != 'nt' or 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.ugettext(context + sep + message) if sep not in tmsg: return tmsg return t.ugettext(message) def ngettext(singular, plural, n): return t.ungettext(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.fromunicode(u) except (LookupError, UnicodeEncodeError): return message class keepgettext(object): def _(self, message, context=''): return {'id': message, 'str': _(message, context)} tortoisehg-4.5.2/tortoisehg/util/gpg.py0000644000175000017500000000166513150123225021020 0ustar sborhosborho00000000000000# 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-4.5.2/tortoisehg/util/pipeui.py0000644000175000017500000001706713150123225021541 0ustar sborhosborho00000000000000# pipeui.py - append parsable label to output, prompt and progress # # Copyright 2014 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. """append parsable label to output, prompt and progress This extension is intended to be used with the command server, so the packed message provides no reliable length field. Message structure:: without label: |msg...| with label: |'\1'|label...|'\n'|msg...| progress: |'\1'|'ui.progress'|'\n'|topic|'\0'|pos|'\0'|item|'\0'|unit|'\0'|total| prompt: |'\1'|'ui.prompt'|'\n'|msg|'\0'|default| Labels: ui.getpass (with ui.prompt) denotes message for password prompt ui.progress contains packed progress data (not for display) ui.promptchoice (with ui.prompt) denotes message and choices for prompt """ import time from mercurial import util from tortoisehg.util import hgversion from tortoisehg.util.i18n import agettext as _ testedwith = hgversion.testedwith class _labeledstr(str): r""" >>> a = _labeledstr('foo', 'ui.warning') >>> a.packed() '\x01ui.warning\nfoo' >>> _labeledstr(a, 'ui.error').packed() '\x01ui.warning ui.error\nfoo' >>> _labeledstr('foo', '').packed() 'foo' >>> _labeledstr('\1foo', '').packed() '\x01\n\x01foo' >>> _labeledstr(a, '') is a # fast path True """ def __new__(cls, s, l): if isinstance(s, cls): if not l: return s if s._label: l = s._label + ' ' + l t = str.__new__(cls, s) t._label = l return t def packed(self): if not self._label and not self.startswith('\1'): return str(self) return '\1%s\n%s' % (self._label, self) def _packmsgs(msgs, label): r""" >>> _packmsgs(['foo'], '') ['foo'] >>> _packmsgs(['foo ', 'bar'], '') ['foo bar'] >>> _packmsgs(['foo ', 'bar'], 'ui.status') ['\x01ui.status\nfoo bar'] >>> _packmsgs(['foo ', _labeledstr('bar', 'log.branch')], '') ['foo ', '\x01log.branch\nbar'] """ if not any(isinstance(e, _labeledstr) for e in msgs): # pack into single message to avoid overhead of label header and # channel protocol; also it's convenient for command-server client # to receive the whole message at once. if len(msgs) > 1: msgs = [''.join(msgs)] if not label: # fast path return msgs return [_labeledstr(e, label).packed() for e in msgs] def splitmsgs(data): r"""Split data to list of packed messages assuming that original messages contain no '\1' character >>> splitmsgs('') [] >>> splitmsgs('\x01ui.warning\nfoo\x01\nbar') ['\x01ui.warning\nfoo', '\x01\nbar'] >>> splitmsgs('foo\x01ui.warning\nbar') ['foo', '\x01ui.warning\nbar'] """ msgs = data.split('\1') if msgs[0]: return msgs[:1] + ['\1' + e for e in msgs[1:]] else: return ['\1' + e for e in msgs[1:]] def unpackmsg(data): r"""Try to unpack data to original message and label >>> unpackmsg('foo') ('foo', '') >>> unpackmsg('\x01ui.warning\nfoo') ('foo', 'ui.warning') >>> unpackmsg('\x01ui.warning') # immature end ('', 'ui.warning') """ if not data.startswith('\1'): return data, '' try: label, msg = data[1:].split('\n', 1) return msg, label except ValueError: return '', data[1:] def _packprompt(msg, default): r""" >>> _packprompt('foo', None) 'foo\x00' >>> _packprompt(_labeledstr('$$ &Yes', 'ui.promptchoice'), 'y').packed() '\x01ui.promptchoice\n$$ &Yes\x00y' """ s = '\0'.join((msg, default or '')) if not isinstance(msg, _labeledstr): return s return _labeledstr(s, msg._label) def unpackprompt(msg): """Try to unpack prompt message to original message and default value""" args = msg.split('\0', 1) if len(args) == 1: return msg, '' else: return args def _fromint(n): if n is None: return '' return str(n) def _toint(s): if not s: return None return int(s) def _packprogress(topic, pos, item, unit, total): r""" >>> _packprogress('updating', 1, 'foo', 'files', 5) 'updating\x001\x00foo\x00files\x005' >>> _packprogress('updating', None, '', '', None) 'updating\x00\x00\x00\x00' """ return '\0'.join((topic, _fromint(pos), item, unit, _fromint(total))) def unpackprogress(msg): r"""Try to unpack progress message to tuple of parameters >>> unpackprogress('updating\x001\x00foo\x00files\x005') ('updating', 1, 'foo', 'files', 5) >>> unpackprogress('updating\x00\x00\x00\x00') ('updating', None, '', '', None) >>> unpackprogress('updating\x001\x00foo\x00files') # immature end ('updating', None, '', '', None) >>> unpackprogress('') # no separator ('', None, '', '', None) >>> unpackprogress('updating\x00a\x00foo\x00files\x005') # invalid pos ('updating', None, '', '', None) """ try: topic, pos, item, unit, total = msg.split('\0') return topic, _toint(pos), item, unit, _toint(total) except ValueError: # fall back to termination topic = msg.split('\0', 1)[0] return topic, None, '', '', None _progressrefresh = 0.1 # [sec] def _extenduiclass(parcls): class pipeui(parcls): _lastprogresstopic = None _lastprogresstime = 0 def write(self, *args, **opts): if self._buffers: # do not label buffered data because it can be written later super(pipeui, self).write(*args, **opts) return label = opts.get('label', '') super(pipeui, self).write(*_packmsgs(args, label), **opts) def write_err(self, *args, **opts): label = opts.get('label', '') super(pipeui, self).write_err(*_packmsgs(args, label), **opts) def prompt(self, msg, default='y'): fullmsg = _packprompt(msg, default) return super(pipeui, self).prompt(fullmsg, default) # write raw prompt value with choices def promptchoice(self, prompt, default=0): _msg, choices = self.extractchoices(prompt) resps = [r for r, _t in choices] prompt = self.label(prompt, 'ui.promptchoice') r = self.prompt(prompt, resps[default]) try: return resps.index(r.lower()) except ValueError: raise util.Abort(_('unrecognized response: %s') % r) def getpass(self, prompt=None, default=None): prompt = self.label(prompt or _('password: '), 'ui.getpass') return super(pipeui, self).getpass(prompt, default) def progress(self, topic, pos, item='', unit='', total=None): now = time.time() if (topic == self._lastprogresstopic and pos is not None and now - self._lastprogresstime < _progressrefresh): # skip busy increment of the same topic return if pos is None: # the topic is about to be closed self._lastprogresstopic = None else: self._lastprogresstopic = topic self._lastprogresstime = now msg = _packprogress(topic, pos, item, unit, total) self.write_err(msg, label='ui.progress') def label(self, msg, label): return _labeledstr(msg, label) return pipeui def uisetup(ui): ui.__class__ = _extenduiclass(ui.__class__) tortoisehg-4.5.2/tortoisehg/util/shlib.py0000644000175000017500000001527213150123225021343 0ustar sborhosborho00000000000000# 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 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.lfstatus = True repostate = repo.status() # will update .hg/dirstate as a side effect 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: f = repo.vfs('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.vfs('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-4.5.2/tortoisehg/util/terminal.py0000644000175000017500000000705113150123225022051 0ustar sborhosborho00000000000000import os, sys from mercurial import util from tortoisehg.util import hglib 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_ = hglib.loadui() 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-4.5.2/tortoisehg/util/bugtraq.py0000644000175000017500000001777113150123225021715 0ustar sborhosborho00000000000000from 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.util.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-4.5.2/tortoisehg/util/__init__.py0000644000175000017500000000001513150123225021766 0ustar sborhosborho00000000000000#placeholder tortoisehg-4.5.2/tortoisehg/hgqt/0000755000175000017500000000000013251112740017651 5ustar sborhosborho00000000000000tortoisehg-4.5.2/tortoisehg/hgqt/grep.py0000644000175000017500000007420513153775104021202 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import cgi import os import re from .qtcore import ( QAbstractTableModel, QMimeData, QModelIndex, QSettings, QThread, QUrl, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAbstractItemView, QAction, QCheckBox, QCompleter, QDialog, QFont, QFrame, QGridLayout, QKeySequence, QLabel, QLineEdit, QHBoxLayout, QMenu, QPushButton, QRadioButton, QShortcut, QTableView, QVBoxLayout, QWidget, ) from mercurial import ( commands, error, hg, match, subrepo, ui, util, ) from ..util import ( hglib, paths, thread2, ) from ..util.i18n import _ from . import ( cmdui, filedialogs, fileview, htmldelegate, htmlui, qtlib, settings, thgrepo, visdiff, ) # 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(str) progress = pyqtSignal(str, object, str, str, object) revisionSelected = pyqtSignal(int) def __init__(self, repoagent, upats, parent=None, **opts): QWidget.__init__(self, parent) self._repoagent = repoagent self.thread = None mainvbox = QVBoxLayout() mainvbox.setSpacing(6) hbox = QHBoxLayout() hbox.setContentsMargins(2, 2, 2, 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) revision.toggled.connect(self._onRevisionToggled) 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) recurse.setChecked(True) working.setChecked(True) working.toggled.connect(self._updateRecurse) history.toggled.connect(self._updateFollow) incle.textChanged.connect(self._updateFollow) excle.textChanged.connect(self._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) 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 = hglib.shortrepoid(repo) s = QSettings() sh = qtlib.readStringList(s, 'grep/search-' + repoid) ph = qtlib.readStringList(s, 'grep/paths-' + repoid) self.pathshistory = [p for p in ph if p] self.searchhistory = [s for s in sh if s] self.setCompleters() mainvbox.setContentsMargins(0, 0, 0, 0) self.setLayout(mainvbox) self._updateRecurse() self._updateFollow() @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)) @pyqtSlot(bool) def _onRevisionToggled(self, checked): self.revle.setEnabled(checked) if checked: self.revle.selectAll() self.revle.setFocus() @pyqtSlot() def _updateRecurse(self): checked = self.wctxradio.isChecked() try: wctx = self.repo[None] if '.hgsubstate' in wctx: self.recurse.setEnabled(checked) else: self.recurse.setEnabled(False) self.recurse.setChecked(False) except util.Abort: self.recurse.setEnabled(False) self.recurse.setChecked(False) @pyqtSlot() def _updateFollow(self): slowpath = bool(self.incle.text() or self.excle.text()) self.follow.setEnabled(self.aradio.isChecked() and not slowpath) if slowpath: self.follow.setChecked(False) 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 and self.thread and self.thread.isRunning()): self.stopClicked() 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 = hglib.shortrepoid(self.repo) 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 = map(str.strip, inc.split(',')) exc = hglib.fromunicode(self.excle.text()) if exc: exc = map(str.strip, 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 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(str) progress = pyqtSignal(str, object, str, str, 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 = cgi.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(self.repo.ui) 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(str) progress = pyqtSignal(str, object, str, str, 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(self.repo.ui) 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.setDragDropMode(QTableView.DragOnly) 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(repoagent, self)) self.selectionModel().selectionChanged.connect(self.onSelectionChanged) @property def repo(self): return self._repoagent.rawRepo() 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): uroot = hglib.tounicode(root) srepoagent = self._repoagent.subRepoAgent(uroot) path = abs[len(root)+1:] 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, repoagent, parent): QAbstractTableModel.__init__(self, parent) self._repoagent = repoagent self.rows = [] self.headers = (_('File'), _('Line'), _('Rev'), _('User'), _('Match Text')) def rowCount(self, parent=QModelIndex()): return len(self.rows) def columnCount(self, parent=QModelIndex()): return len(self.headers) def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None if role == Qt.DisplayRole: return self.rows[index.row()][index.column()] return None def headerData(self, col, orientation, role=Qt.DisplayRole): if role != Qt.DisplayRole or orientation != Qt.Horizontal: return None else: return self.headers[col] def flags(self, index): flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled return flags def mimeTypes(self): return ['text/uri-list'] def mimeData(self, indexes): snapshots = {} for index in indexes: if index.column() != 0: continue path, line, rev, user, text = self.rows[index.row()] 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: repo = self._repoagent.rawRepo() lpaths = map(hglib.fromunicode, paths) lbase, _ = visdiff.snapshot(repo, lpaths, repo[rev]) base = hglib.tounicode(lbase) else: base = self._repoagent.rootPath() for p in paths: urls.append(QUrl.fromLocalFile(os.path.join(base, p))) m = QMimeData() m.setUrls(urls) return m 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.setWindowTitle(_('TortoiseHg Search')) outervbox = QVBoxLayout() outervbox.setContentsMargins(5, 5, 5, 0) self.setLayout(outervbox) self._searchwidget = SearchWidget(repoagent, upats, parent=self, **opts) outervbox.addWidget(self._searchwidget) self._stbar = cmdui.ThgStatusBar() outervbox.addWidget(self._stbar) self._searchwidget.showMessage.connect(self._stbar.showMessage) self._searchwidget.progress.connect(self._stbar.progress) self.resize(800, 550) 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-4.5.2/tortoisehg/hgqt/branchop.py0000644000175000017500000001042613150123225022020 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import from .qtgui import ( QComboBox, QDialog, QDialogButtonBox, QGridLayout, QKeySequence, QLabel, QRadioButton, QShortcut, QVBoxLayout, ) from ..util import hglib from ..util.i18n import _ from . import qtlib class BranchOpDialog(QDialog): 'Dialog for manipulating wctx.branch()' def __init__(self, repoagent, oldbranchop, parent=None): QDialog.__init__(self, parent) self.setWindowTitle(_('%s - branch operation') % repoagent.displayName()) self.setWindowIcon(qtlib.geticon('hg-branch')) layout = QVBoxLayout() self.setLayout(layout) repo = repoagent.rawRepo() 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 hglib.namedbranches(repo): 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: bc = branchCombo i = bc.findText(oldbranchop) if i >= 0: bc.setCurrentIndex(i) else: bc.addItem(oldbranchop) bc.setCurrentIndex(bc.count() - 1) 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 unicode - open new named branch ''' if self.branchCombo.isEnabled(): # branch name cannot start/end with whitespace (see dirstate._branch) self.branchop = unicode(self.branchCombo.currentText()).strip() elif self.closebranch.isChecked(): self.branchop = False else: self.branchop = None QDialog.accept(self) tortoisehg-4.5.2/tortoisehg/hgqt/infobar.py0000644000175000017500000003251613150123225021650 0ustar sborhosborho00000000000000# infobar.py - widget for non-modal message # # Copyright 2011 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. from __future__ import absolute_import import re import urllib from .qtcore import ( QTimer, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QColor, QDialogButtonBox, QFrame, QHBoxLayout, QLabel, QPalette, QPushButton, QSizePolicy, QStyle, QTreeView, QVBoxLayout, QWidget, ) from mercurial.i18n import _ as hggettext from ..util import hglib from ..util.i18n import _ from . import qtlib # 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', '
') # type of InfoBar (the number denotes its priority) INFO = 1 ERROR = 2 CONFIRM = 3 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(str) 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_TitleBarCloseButton)) if qtlib.IS_RETINA: self._closebutton.setIconSize(qtlib.barRetinaIconSize()) 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 = 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 = 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 InfoBarPlaceholder(QWidget): """Manage geometry of view according to visibility of InfoBar""" linkActivated = pyqtSignal(str) def __init__(self, repoagent, parent=None): super(InfoBarPlaceholder, self).__init__(parent) self._repoagent = repoagent lay = QVBoxLayout() lay.setSpacing(0) lay.setContentsMargins(0, 0, 0, 0) self.setLayout(lay) self._view = None self._activeInfoBar = None self._infoLifetime = QTimer(self, singleShot=True) self._infoLifetime.timeout.connect(self._clearStaleInfo) repoagent.busyChanged.connect(self._clearStaleInfo) def setView(self, view): assert isinstance(view, QTreeView) lay = self.layout() if self._view: lay.removeWidget(self._view) self._view = view lay.addWidget(view) def activeInfoBar(self): return self._activeInfoBar def setInfoBar(self, cls, *args, **kwargs): """Show the given infobar at top of the widget 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.linkActivated) self._activeInfoBar = w self._updateInfoBarGeometry() w.show() if w.infobartype > INFO: w.setFocus() # to handle key press by InfoBar else: self._infoLifetime.start(2000) return w 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 def discardInfoBar(self): """Remove current infobar silently with no signal""" if self._activeInfoBar: self._activeInfoBar.hide() self._freeInfoBar() @pyqtSlot() def _freeInfoBar(self): """Disown closed infobar""" if not self._activeInfoBar: return self._activeInfoBar.setParent(None) self._activeInfoBar = None self._infoLifetime.stop() # clear margin for overlay self.layout().setContentsMargins(0, 0, 0, 0) @pyqtSlot() def _clearStaleInfo(self): # do not clear message while command is running because it doubles # as busy indicator if self._repoagent.isBusy() or self._infoLifetime.isActive(): return self.clearInfoBar(INFO) def _updateInfoBarGeometry(self): if not self._activeInfoBar: return w = self._activeInfoBar f = self w.setGeometry(0, 0, f.width(), w.heightForWidth(f.width())) # give margin to make header or first row accessible. without header, # column width cannot be changed while confirmation is presented. # # CONFIRM ERROR INFO # ____________ .... .... ____ # : cmy ____ cmy ---- h.y # : w.height ____ ---- h.y ____ h.height # _:__________ ---- h.y ____ h.height # ____ h.height # h = self._view.header() if w.infobartype >= CONFIRM: cmy = w.height() - h.y() elif w.infobartype >= ERROR: cmy = w.height() - h.y() - h.height() else: cmy = 0 self.layout().setContentsMargins(0, max(cmy, 0), 0, 0) def resizeEvent(self, event): super(InfoBarPlaceholder, self).resizeEvent(event) self._updateInfoBarGeometry() @pyqtSlot(str) def showMessage(self, msg): if msg: self.setInfoBar(StatusInfoBar, msg) else: self.clearInfoBar(priority=StatusInfoBar.infobartype) @pyqtSlot(str, str) def showOutput(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 = linkifyMessage('\n'.join(infolines), subrepo=subrepo) self.setInfoBar(CommandErrorInfoBar, infomsg) tortoisehg-4.5.2/tortoisehg/hgqt/filedata.py0000644000175000017500000007311413251112733022004 0ustar sborhosborho00000000000000# 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, posixpath import cStringIO from mercurial import commands, error, match, patch, subrepo, util from mercurial import copies from mercurial.node import nullrev from tortoisehg.util import hglib, patchctx from tortoisehg.util.i18n import _ from tortoisehg.hgqt import fileencoding forcedisplaymsg = _('Display the file anyway') class _BadContent(Exception): """Failed to display file because it is binary or size limit exceeded""" def _exceedsMaxLineLength(data, maxlength=100000): if len(data) < maxlength: return False for line in data.splitlines(): if len(line) > maxlength: return True return False def _checkdifferror(data, maxdiff): p = _('Diff not displayed: ') size = len(data) if size > maxdiff: return p + _('File is larger than the specified max size.\n' 'maxdiff = %s KB') % (maxdiff // 1024) elif '\0' in data: return p + _('File is binary') elif _exceedsMaxLineLength(data): # it's incredibly slow to render long line by QScintilla return p + _('File may be binary (maximum line length exceeded)') def _trimdiffheader(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 = diff.split('\n', 3) if len(out) == 4: return out[3] else: # there was an error or rename without diffs return diff class _AbstractFileData(object): def __init__(self, ctx, ctx2, wfile, status=None, rpath=None): self._ctx = ctx self._pctx = ctx2 self._wfile = wfile self._status = status self._rpath = rpath or '' self.contents = None self.ucontents = None self.error = None self.olddata = None self.diff = None self.flabel = u'' self.elabel = u'' self.changes = None self._textencoding = fileencoding.contentencoding(ctx._repo.ui) def createRebased(self, pctx): # new status is not known return self.__class__(self._ctx, pctx, self._wfile, rpath=self._rpath) def load(self, changeselect=False, force=False): # Note: changeselect may be set to True even if the underlying data # isn't chunk-selectable raise NotImplementedError def __eq__(self, other): # unlike changectx, this also compares hash in case it was stripped and # recreated. FileData may live longer than changectx in Mercurial. return (isinstance(other, self.__class__) and self._ctx == other._ctx and self._ctx.node() == other._ctx.node() and self._pctx == other._pctx and (self._pctx is None or self._pctx.node() == other._pctx.node()) and self._wfile == other._wfile and self._rpath == other._rpath) def __ne__(self, other): return not self == other def __hash__(self): return hash((self._ctx, self._ctx.node(), self._pctx, self._pctx is None or self._pctx.node(), self._wfile, self._rpath)) def __repr__(self): return '<%s %s@%s>' % (self.__class__.__name__, posixpath.join(self._rpath, self._wfile), self._ctx) def isLoaded(self): loadables = [self.contents, self.ucontents, self.error, self.diff] return any(e is not None for e in loadables) def isNull(self): return self._ctx.rev() == nullrev and not self._wfile def isValid(self): return self.error is None and not self.isNull() def rev(self): return self._ctx.rev() def baseRev(self): return self._pctx.rev() def parentRevs(self): # may contain nullrev, which allows "nullrev in parentRevs()" return [p.rev() for p in self._ctx.parents()] def rawContext(self): return self._ctx def rawBaseContext(self): return self._pctx # absoluteFilePath : "C:\\Documents\\repo\\foo\\subrepo\\bar\\baz" # absoluteRepoRootPath: "C:\\Documents\\repo\\foo\\subrepo" # canonicalFilePath: "bar/baz" # filePath : "foo/subrepo/bar/baz" # repoRootPath : "foo/subrepo" def absoluteFilePath(self): """Absolute file-system path of this file""" repo = self._ctx._repo return hglib.tounicode(os.path.normpath(repo.wjoin(self._wfile))) def absoluteRepoRootPath(self): """Absolute file-system path to the root of the container repository""" # repo.root should be normalized repo = self._ctx._repo return hglib.tounicode(repo.root) def canonicalFilePath(self): """Path relative to the repository root which contains this file""" return hglib.tounicode(self._wfile) def filePath(self): """Path relative to the top repository root in the current context""" return hglib.tounicode(posixpath.join(self._rpath, self._wfile)) def repoRootPath(self): """Root path of the container repository relative to the top repository in the current context; '' for top repository""" return hglib.tounicode(self._rpath) def fileStatus(self): return self._status def isDir(self): return not self._wfile def mergeStatus(self): pass def subrepoType(self): pass def textEncoding(self): return self._textencoding def setTextEncoding(self, name): self._textencoding = fileencoding.canonname(name) def detectTextEncoding(self): ui = self._ctx._repo.ui # use file content for better guess; diff may be mixed encoding or # have immature multi-byte sequence data = self.contents or self.diff or '' fallbackenc = self._textencoding self._textencoding = fileencoding.guessencoding(ui, data, fallbackenc) def _textToUnicode(self, s): return s.decode(self._textencoding, 'replace') def diffText(self): return self._textToUnicode(self.diff or '') def fileText(self): return self._textToUnicode(self.contents or '') class FileData(_AbstractFileData): def __init__(self, ctx, pctx, path, status=None, rpath=None, mstatus=None): super(FileData, self).__init__(ctx, pctx, path, status, rpath) self._mstatus = mstatus def load(self, changeselect=False, force=False): if self.rev() == nullrev: return ctx = self._ctx ctx2 = self._pctx wfile = self._wfile status = self._status errorprefix = _('File or diffs not displayed: ') try: self._readStatus(ctx, ctx2, wfile, status, changeselect, force) except _BadContent, e: self.error = errorprefix + e.args[0] + '\n\n' + forcedisplaymsg except (EnvironmentError, error.LookupError, util.Abort), e: self.error = errorprefix + hglib.tounicode(str(e)) def _checkMaxDiff(self, ctx, wfile, maxdiff, force): self.error = None 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()) if not force and size > maxdiff: raise _BadContent(_('File is larger than the specified max size.\n' 'maxdiff = %s KB') % (maxdiff // 1024)) data = fctx.data() if not force: if '\0' in data or ctx.isStandin(wfile): raise _BadContent(_('File is binary')) elif _exceedsMaxLineLength(data): # it's incredibly slow to render long line by QScintilla raise _BadContent(_('File may be binary (maximum line length ' 'exceeded)')) return fctx, data def _checkRenamed(self, repo, ctx, pctx, wfile): m = match.exact(repo, '', [wfile]) copy = copies.pathcopies(pctx, ctx, match=m) oldname = copy.get(wfile) if not oldname: self.flabel += _(' (was added)') return fr = hglib.tounicode(oldname) if oldname in ctx: self.flabel += _(' (copied from %s)') % fr else: self.flabel += _(' (renamed from %s)') % fr return oldname 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' % self.filePath() 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 ctx2 is None: ctx2 = ctx.p1() if status is None: status = getstatus(repo, ctx2.node(), ctx.node(), wfile) 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 try: fctx, newdata = self._checkMaxDiff(ctx, wfile, maxdiff, force) except _BadContent: if status == 'A': self._checkRenamed(repo, ctx, ctx2, wfile) raise 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': oldname = self._checkRenamed(repo, ctx, ctx2, wfile) if not oldname: return olddata = ctx2[oldname].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() copy = {} if oldname != wfile: copy[wfile] = oldname patches = patch.diff(repo, ctx.node(), None, match=m, opts=diffopts, copy=copy) for c in patches: fp.write(c) fp.seek(0) # feed diffs through parsepatch() for more fine grained # chunk selection filediffs = patch.parsepatch(fp) if filediffs and filediffs[0].hunks: 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 = hglib.unidifftext(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 = '' def mergeStatus(self): return self._mstatus def diffText(self): udiff = self._textToUnicode(self.diff or '') if self.changes: return udiff return _trimdiffheader(udiff) def setChunkExcluded(self, chunk, exclude): assert chunk in self.changes.hunks if chunk.excluded == exclude: return if exclude: chunk.excluded = True self.changes.excludecount += 1 else: chunk.excluded = False self.changes.excludecount -= 1 class DirData(_AbstractFileData): def load(self, changeselect=False, force=False): self.error = None self.flabel = u'%s' % self.filePath() # TODO: enforce maxdiff before generating diff? ctx = self._ctx pctx = self._pctx try: m = ctx.match(['path:%s' % self._wfile]) self.diff = ''.join(ctx.diff(pctx, m)) except (EnvironmentError, util.Abort), e: self.error = hglib.tounicode(str(e)) return if not force: self.error = _checkdifferror(self.diff, ctx._repo.maxdiff) if self.error: self.error += u'\n\n' + forcedisplaymsg def isDir(self): return True class PatchFileData(_AbstractFileData): def load(self, changeselect=False, force=False): ctx = self._ctx wfile = self._wfile maxdiff = ctx._repo.maxdiff self.error = None self.flabel = u'%s' % self.filePath() try: self.diff = ctx.thgmqpatchdata(wfile) flags = ctx.flags(wfile) except EnvironmentError, e: self.error = hglib.tounicode(str(e)) return 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: self.error = _checkdifferror(self.diff, maxdiff) if self.error: self.error += u'\n\n' + forcedisplaymsg def rev(self): # avoid mixing integer and localstr return nullrev def baseRev(self): # patch has no comparison base return nullrev def diffText(self): return _trimdiffheader(self._textToUnicode(self.diff or '')) class PatchDirData(_AbstractFileData): def load(self, changeselect=False, force=False): self.error = None self.flabel = u'%s' % self.filePath() ctx = self._ctx try: self.diff = ''.join([ctx.thgmqpatchdata(f) for f in ctx.files() if f.startswith(self._wfile + '/')]) except EnvironmentError, e: self.error = hglib.tounicode(str(e)) return if not force: self.error = _checkdifferror(self.diff, ctx._repo.maxdiff) if self.error: self.error += u'\n\n' + forcedisplaymsg def rev(self): # avoid mixing integer and localstr return nullrev def baseRev(self): # patch has no comparison base return nullrev def isDir(self): return True class SubrepoData(_AbstractFileData): def __init__(self, ctx, pctx, path, status, rpath, subkind): super(SubrepoData, self).__init__(ctx, pctx, path, status, rpath) self._subkind = subkind def createRebased(self, pctx): # new status should be unknown, but currently it is 'S' assert self._status == 'S' # TODO: replace 'S' by subrepo's status return self.__class__(self._ctx, pctx, self._wfile, status=self._status, rpath=self._rpath, subkind=self._subkind) def load(self, changeselect=False, force=False): ctx = self._ctx ctx2 = self._pctx if ctx2 is None: ctx2 = ctx.p1() wfile = self._wfile self.error = None self.flabel = u'%s' % self.filePath() try: 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 = [] # TODO: should be copied from the baseui _ui = hglib.loadui() _ui.setconfig('ui', 'paginate', 'off', 'subrepodata') 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)) def isDir(self): return True def subrepoType(self): return self._subkind def createFileData(ctx, ctx2, wfile, status=None, rpath=None, mstatus=None): if isinstance(ctx, patchctx.patchctx): if mstatus: raise ValueError('invalid merge status for patch: %r' % mstatus) return PatchFileData(ctx, ctx2, wfile, status, rpath) return FileData(ctx, ctx2, wfile, status, rpath, mstatus) def createDirData(ctx, pctx, path, rpath=None): if isinstance(ctx, patchctx.patchctx): return PatchDirData(ctx, pctx, path, rpath=rpath) return DirData(ctx, pctx, path, rpath=rpath) def createSubrepoData(ctx, pctx, path, status=None, rpath=None, subkind=None): if not subkind: subkind = ctx.substate.get(path, subrepo.nullstate)[2] # TODO: replace 'S' by subrepo's status return SubrepoData(ctx, pctx, path, 'S', rpath, subkind) def createNullData(repo): ctx = repo[nullrev] fd = FileData(ctx, ctx.p1(), '', 'C') return fd tortoisehg-4.5.2/tortoisehg/hgqt/repoview.py0000644000175000017500000006706113242607601022102 0ustar sborhosborho00000000000000# Copyright (c) 2009-2010 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # Copyright 2010 Steve Borho # # 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 __future__ import absolute_import from math import sin, cos, pi from .qtcore import ( QItemSelectionModel, QPoint, QPointF, QRectF, QSettings, QSize, QT_API, QT_VERSION, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAbstractItemView, QAction, QApplication, QBrush, QClipboard, QColor, QDialog, QDialogButtonBox, QFont, QFontMetrics, QLabel, QListView, QListWidget, QListWidgetItem, QMenu, QPainter, QPainterPath, QPen, QPolygonF, QProxyStyle, QStyle, QStyleOptionViewItemV2, QStyleOptionViewItemV4, QStyledItemDelegate, QTreeView, QVBoxLayout, ) from mercurial import error from ..util import hglib from ..util.i18n import _ from . import ( graph, qtlib, repomodel, ) class HgRepoView(QTreeView): revisionSelected = pyqtSignal(object) revisionActivated = pyqtSignal(object) menuRequested = pyqtSignal(QPoint, object) showMessage = pyqtSignal(str) columnsVisibilityChanged = pyqtSignal() def __init__(self, repoagent, cfgname, colselect, parent=None): QTreeView.__init__(self, parent) self._repoagent = repoagent self.current_rev = -1 self.resized = False self.cfgname = cfgname self.colselect = colselect header = self.header() if QT_VERSION < 0x50000: header.setClickable(False) header.setMovable(True) else: header.setSectionsClickable(False) header.setSectionsMovable(True) header.setDefaultAlignment(Qt.AlignLeft) header.setHighlightSections(False) header.setContextMenuPolicy(Qt.CustomContextMenu) header.customContextMenuRequested.connect(self.headerMenuRequest) header.sectionMoved.connect(self.columnsVisibilityChanged) header.sectionMoved.connect(self._saveColumnSettings) self.createActions() self.setItemDelegateForColumn(repomodel.GraphColumn, GraphDelegate(self)) self.setItemDelegateForColumn(repomodel.DescColumn, LabeledDelegate(self)) self.setItemDelegateForColumn(repomodel.ChangesColumn, LabeledDelegate(self, margin=0)) self.setAcceptDrops(True) self.setDefaultDropAction(Qt.MoveAction) self.setDragEnabled(True) self.setDropIndicatorShown(True) self.setDragDropMode(QAbstractItemView.InternalMove) self.setAllColumnsShowFocus(True) self.setItemsExpandable(False) self.setRootIsDecorated(False) self.setUniformRowHeights(True) # do not pass self.style() to HgRepoViewStyle() because it would steal # the ownership from QApplication and cause SEGV after the deletion of # this widget. self.setStyle(HgRepoViewStyle()) self._paletteswitcher = qtlib.PaletteSwitcher(self) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.doubleClicked.connect(self.revActivated) self.clicked.connect(self.revClicked) @property def repo(self): return self._repoagent.rawRepo() def mousePressEvent(self, event): index = self.indexAt(event.pos()) if not index.isValid(): return if event.button() == Qt.MidButton: self.gotoAncestor(index) return QTreeView.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) act = QAction(_('&Resize Columns'), self) act.triggered.connect(self._resizeIgnoreSettings) menu.addAction(act) self.headermenu = menu @pyqtSlot(QPoint) def headerMenuRequest(self, point): self.headermenu.exec_(self.header().mapToGlobal(point)) def setHistoryColumns(self): dlg = ColumnSelectDialog(self.colselect[1], self.model(), self.visibleColumns()) if dlg.exec_() == QDialog.Accepted: self.setVisibleColumns(dlg.selectedColumns()) self.resizeColumns() self._saveColumnSettings() # for new repository tab def _loadColumnSettings(self): model = self.model() s = QSettings() s.beginGroup(self.colselect[0]) cols = qtlib.readStringList(s, 'columns') cols = [str(col) for col in cols] # Fixup older names for columns if 'Log' in cols: cols[cols.index('Log')] = 'Description' s.setValue('columns', cols) if 'ID' in cols: cols[cols.index('ID')] = 'Rev' s.setValue('columns', cols) s.endGroup() allcolumns = model.allColumns() validcols = [col for col in cols if col in allcolumns] if not validcols: validcols = model._defaultcolumns self.setVisibleColumns(validcols) @pyqtSlot() def _saveColumnSettings(self): s = QSettings() s.beginGroup(self.colselect[0]) s.setValue('columns', self.visibleColumns()) s.endGroup() def visibleColumns(self): hh = self.header() return [self.model().allColumns()[hh.logicalIndex(visualindex)] for visualindex in xrange(hh.count() - hh.hiddenSectionCount())] def setVisibleColumns(self, visiblecols): if not self.model() or visiblecols == self.visibleColumns(): return hh = self.header() hh.sectionMoved.disconnect(self.columnsVisibilityChanged) allcolumns = self.model().allColumns() for logicalindex, colname in enumerate(allcolumns): hh.setSectionHidden(logicalindex, colname not in visiblecols) for newvisualindex, colname in enumerate(visiblecols): logicalindex = allcolumns.index(colname) hh.moveSection(hh.visualIndex(logicalindex), newvisualindex) hh.sectionMoved.connect(self.columnsVisibilityChanged) self.columnsVisibilityChanged.emit() def setModel(self, model): oldmodel = self.model() QTreeView.setModel(self, model) if type(oldmodel) is not type(model): # logical columns are vary by model class self._loadColumnSettings() #Check if the font contains the glyph needed by the model if not QFontMetrics(self.font()).inFont(u'\u2605'): model.unicodestar = False if not QFontMetrics(self.font()).inFont(u'\u2327'): model.unicodexinabox = False self.selectionModel().currentRowChanged.connect(self.onRowChange) self._rev_history = [] self._rev_pos = -1 self._in_history = False @pyqtSlot() def _resizeIgnoreSettings(self): self.resizeColumns(False) @pyqtSlot() def resizeColumns(self, usesettings=True): if not self.model(): return col_widths = [] if usesettings: qs = QSettings() key = '%s/column_widths/%s' % (self.cfgname, hglib.shortrepoid(self.repo)) try: col_widths = [int(w) for w in qtlib.readStringList(qs, key)] except ValueError: pass hh = self.header() hh.setStretchLastSection(False) self._resizeColumns(col_widths) hh.setStretchLastSection(True) self.resized = True def _resizeColumns(self, col_widths): # _resizeColumns misbehaves if called with last section streched hh = self.header() model = self.model() fontm = QFontMetrics(self.font()) for c in range(model.columnCount()): if hh.isSectionHidden(c): continue if c < len(col_widths) and col_widths[c] > 0: w = col_widths[c] else: w = model.maxWidthValueForColumn(c) if isinstance(w, int): pass elif w is not None: w = fontm.width(hglib.tounicode(str(w)) + 'w') else: w = super(HgRepoView, self).sizeHintForColumn(c) self.setColumnWidth(c, w) 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() if clip.supportsSelection(): clip.setText(str(self.repo[rev]), QClipboard.Selection) 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.isValid(): 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.isValid(): 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): 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.isValid(): 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()): col_widths.append(self.columnWidth(c)) try: key = '%s/column_widths/%s' % (self.cfgname, hglib.shortrepoid(self.repo)) s.setValue(key, col_widths) except EnvironmentError: pass self._saveColumnSettings() 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()): if c == repomodel.DescColumn: #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(QProxyStyle): "Override a style's drawPrimitive method to customize the drop indicator" 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: super(HgRepoViewStyle, self).drawPrimitive(element, option, painter, widget) def get_style(line_type, active): if line_type == graph.LINE_TYPE_GRAFT: return Qt.DashLine if line_type == graph.LINE_TYPE_OBSOLETE: return Qt.DotLine return Qt.SolidLine def get_width(line_type, active): if line_type >= graph.LINE_TYPE_FAMILY or not active: return 1 return 2 def _edge_color(edge, active): if not active or edge.linktype == graph.LINE_TYPE_FAMILY: return "gray" else: colors = graph.COLORS return colors[edge.color % len(colors)] class GraphDelegate(QStyledItemDelegate): def __init__(self, parent=None): super(GraphDelegate, self).__init__(parent) self._rowheight = 16 # updated to the actual height on paint() def _col2x(self, col): maxradius = max(self._rowheight / 2, 1) return maxradius * (col + 1) def _colcount(self, width): maxradius = max(self._rowheight / 2, 1) return (width + maxradius - 1) // maxradius def _dotradius(self): return 0.4 * self._rowheight def paint(self, painter, option, index): QStyledItemDelegate.paint(self, painter, option, index) # update to the actual height that should be the same for all rows self._rowheight = option.rect.height() visibleend = self._colcount(option.rect.width()) gnode = index.data(repomodel.GraphNodeRole) painter.save() try: painter.setClipRect(option.rect) painter.setRenderHint(QPainter.Antialiasing) painter.translate(option.rect.topLeft()) self._drawEdges(painter, index, gnode, visibleend) if gnode.x < visibleend: self._drawNode(painter, index, gnode) finally: painter.restore() def _drawEdges(self, painter, index, gnode, visibleend): h = self._rowheight dot_y = h / 2 def isactive(e): m = index.model() return m.isActiveRev(e.startrev) and m.isActiveRev(e.endrev) def lineimportance(pe): return isactive(pe[1]), pe[1].importance 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 # omit invisible lines lines = [((start, end), e) for (start, end), e in lines if start < visibleend or end < visibleend] # remove hidden lines that can be partly visible due to antialiasing lines = dict(sorted(lines, key=lineimportance)).items() # still necessary to sort by importance because lines can partially # overlap near contact point lines.sort(key=lineimportance) for (start, end), e in lines: active = isactive(e) lpen = QPen(QColor(_edge_color(e, active))) lpen.setStyle(get_style(e.linktype, active)) lpen.setWidth(get_width(e.linktype, active)) painter.setPen(lpen) x1 = self._col2x(start) x2 = self._col2x(end) if x1 == x2: painter.drawLine(x1, y1, x2, y4) else: 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) def _drawNode(self, painter, index, gnode): m = index.model() if not m.isActiveRev(gnode.rev): dot_color = QColor("gray") radius = self._dotradius() * 0.8 else: fg = index.data(Qt.ForegroundRole) if not fg: # work around integrity error in HgRepoListModel, which may # provide a valid index for stripped revision and data() # returns None due to RepoLookupError. (#4451) return dot_color = QBrush(fg).color() 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 = self._rowheight / 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 polygon(r, sides, shift=0): """draw a regular poligon, pointing upward""" phaseincr = (2 * pi / sides) phaseshift = phaseincr * shift phases = [phaseshift + phaseincr * n for n in range(sides)] points = [QPointF(centre_x + r * -sin(p), centre_y + r * -cos(p)) for p in phases] points.append(points[-1]) poly = QPolygonF(points) painter.drawPolygon(poly) 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) def square(r): rect = QRectF(centre_x - r, centre_y - r, 2 * r, 2 * r) painter.drawRect(rect) if gnode.shape == graph.NODE_SHAPE_APPLIEDPATCH: symbolsize = radius / 1.5 symbol = diamond elif gnode.shape == graph.NODE_SHAPE_UNAPPLIEDPATCH: symbolsize = radius / 1.5 fillcolor = QColor('#dddddd') painter.setPen(fillcolor) symbol = diamond elif gnode.shape == graph.NODE_SHAPE_CLOSEDBRANCH: symbolsize = 0.5 * radius symbol = closesymbol elif gnode.shape == graph.NODE_SHAPE_REVISION_SECRET: symbolsize = 0.45 * radius symbol = square elif gnode.shape == graph.NODE_SHAPE_REVISION_DRAFT: symbolsize = 0.57 * radius symbol = lambda size: polygon(size, 5) else: symbolsize = 0.5 * radius symbol = circle if gnode.faded: painter.setBrush(truewhite) painter.setPen(truewhite) white.setAlpha(64) fillcolor.setAlpha(64) symbol(symbolsize) pencolor.setAlpha(64) pen.setColor(pencolor) painter.setPen(pen) if gnode.wdparent and gnode.shape != graph.NODE_SHAPE_CLOSEDBRANCH: painter.setBrush(white) symbol(2 * 0.9 * symbolsize) painter.setBrush(fillcolor) symbol(symbolsize) def sizeHint(self, option, index): size = super(GraphDelegate, self).sizeHint(option, index) gnode = index.data(repomodel.GraphNodeRole) if gnode: # return width for current height assuming that row height # is calculated first (mimic width-for-height policy) return QSize(self._col2x(gnode.cols), max(size.height(), 16)) else: return size class _LabelsLayout(object): """Lay out and render text labels""" def __init__(self, labels, font, margin=2): self._labels = labels self._margin = margin if font.bold(): # cancel bold of working-directory row font = QFont(font) font.setBold(False) self._font = font fm = QFontMetrics(font) self._twidths = [fm.width(t) for t, _s in labels] self._th = fm.height() self._padw = 2 self._padh = 1 # may overwrite horizontal frame to fit row def width(self): space = 2 * self._padw + self._margin return sum(self._twidths) + len(self._labels) * space - self._margin def height(self): return self._th + 2 * self._padh def draw(self, painter, pos): painter.save() try: painter.translate(pos) self._drawLabels(painter) finally: painter.restore() def _drawLabels(self, painter): th = self._th padw = self._padw padh = self._padh painter.setFont(self._font) x = 0 for (text, style), tw in zip(self._labels, self._twidths): lw = tw + 2 * padw lh = th + 2 * padh # draw bevel, background and text in order bg = qtlib.getbgcoloreffect(style) painter.fillRect(x, 0, lw, lh, bg.darker(110)) painter.fillRect(x + 1, 1, lw - 2, lh - 2, bg.lighter(110)) painter.fillRect(x + 2, 2, lw - 4, lh - 4, bg) painter.setPen(qtlib.gettextcoloreffect(style)) painter.drawText(x + padw, padh, tw, th, 0, text) x += lw + self._margin class LabeledDelegate(QStyledItemDelegate): """Render text labels in place of icon/pixmap decoration""" def __init__(self, parent=None, margin=2): super(LabeledDelegate, self).__init__(parent) self._margin = margin def _makeLabelsLayout(self, labels, option): return _LabelsLayout(labels, option.font, self._margin) def initStyleOption(self, option, index): super(LabeledDelegate, self).initStyleOption(option, index) labels = index.data(repomodel.LabelsRole) if not labels: return lay = self._makeLabelsLayout(labels, option) option.decorationSize = QSize(lay.width(), lay.height()) if isinstance(option, QStyleOptionViewItemV2): option.features |= QStyleOptionViewItemV2.HasDecoration def paint(self, painter, option, index): super(LabeledDelegate, self).paint(painter, option, index) labels = index.data(repomodel.LabelsRole) if not labels: return option = QStyleOptionViewItemV4(option) self.initStyleOption(option, index) if option.widget: style = option.widget.style() else: style = QApplication.style() rect = style.subElementRect(QStyle.SE_ItemViewItemDecoration, option, option.widget) # for maximum readability, use vivid color regardless of option.state lay = self._makeLabelsLayout(labels, option) painter.save() try: painter.setClipRect(option.rect) lay.draw(painter, rect.topLeft()) finally: painter.restore() def sizeHint(self, option, index): size = super(LabeledDelegate, self).sizeHint(option, index) # give additional margins for each row (even if it has no labels # because uniformRowHeights is enabled) option = QStyleOptionViewItemV4(option) self.initStyleOption(option, index) lay = self._makeLabelsLayout([], option) return QSize(size.width(), max(size.height(), lay.height() + 2)) class ColumnSelectDialog(QDialog): def __init__(self, name, model, curcolumns, parent=None): QDialog.__init__(self, parent) all = model.allColumns() colnames = dict(model.allColumnHeaders()) self.setWindowTitle(name) self.setWindowFlags(self.windowFlags() & \ ~Qt.WindowContextHelpButtonHint) self.setMinimumSize(250, 265) disabled = [c for c in all if c not in curcolumns] layout = QVBoxLayout() layout.setContentsMargins(5, 5, 5, 5) self.setLayout(layout) list = QListWidget() # enabled cols are listed in sorted order, disabled are listed last for c in curcolumns + disabled: item = QListWidgetItem(colnames[c]) item.columnid = c item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled | Qt.ItemIsUserCheckable) if c in curcolumns: item.setCheckState(Qt.Checked) else: 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) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) layout.addWidget(bb) def selectedColumns(self): cols = [] for i in xrange(self.list.count()): item = self.list.item(i) if item.checkState() == Qt.Checked: cols.append(item.columnid) return cols tortoisehg-4.5.2/tortoisehg/hgqt/thgimport.py0000644000175000017500000002460313150123225022243 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import shutil import tempfile from .qtcore import ( QDir, QTimer, Qt, pyqtSlot, ) from .qtgui import ( QApplication, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QGridLayout, QHBoxLayout, QKeySequence, QLabel, QPushButton, QVBoxLayout, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdui, commit, cslist, qtlib, ) _FILE_FILTER = "%s;;%s" % (_("Patch files (*.diff *.patch)"), _("All files (*)")) def _writetempfile(text): fd, filename = tempfile.mkstemp(suffix='.patch', prefix='thg-import-', dir=qtlib.gettempdir()) try: os.write(fd, text) finally: os.close(fd) return filename # TODO: handle --mq options from command line or MQ widget class ImportDialog(QDialog): """Dialog to import patches""" def __init__(self, repoagent, parent, **opts): super(ImportDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint | Qt.WindowMaximizeButtonHint) self.setWindowTitle(_('Import - %s') % repoagent.displayName()) self.setWindowIcon(qtlib.geticon('hg-import')) self._repoagent = repoagent # base layout box box = QVBoxLayout() box.setSpacing(6) self.setLayout(box) ## main layout grid self.grid = grid = QGridLayout() grid.setSpacing(6) box.addLayout(grid, 1) ### 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.addLayout(statbox, 3, 1) ## command widget self._cmdcontrol = cmd = cmdui.CmdSessionControlWidget(self) cmd.finished.connect(self.done) cmd.linkActivated.connect(self.commitActivated) box.addWidget(cmd) cmd.showStatusMessage(_('Checking working directory status...')) QTimer.singleShot(0, self.checkStatus) self._runbutton = cmd.addButton(_('&Import'), QDialogButtonBox.AcceptRole) self._runbutton.clicked.connect(self._runCommand) grid.setRowStretch(cslistrow, 1) grid.setColumnStretch(cslistcol, 1) # signal handlers self.src_combo.editTextChanged.connect(self.preview) self.p0chk.toggled.connect(self.preview) # prepare to show self.src_combo.lineEdit().selectAll() 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.invalidatedirstate() 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._cmdcontrol.showStatusMessage(text) else: self._cmdcontrol.showStatusMessage('') 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, _filter = QFileDialog.getOpenFileNames( self, caption, self._repoagent.rootPath(), _FILE_FILTER) if filelist: # Qt file browser uses '/' in paths, even on Windows. nl = [unicode(QDir.toNativeSeparators(x)) for x in filelist] self.src_combo.setEditText(os.pathsep.join(nl)) self.src_combo.setFocus() def browsedir(self): caption = _("Select Directory containing patches") path = QFileDialog.getExistingDirectory(self, caption, self._repoagent.rootPath()) 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 = _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) @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() else: self.cslist.update([os.path.abspath(p) for p in patches]) self.updatestatus() self._updateUi() 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)) @pyqtSlot() def _runCommand(self): if self.cslist.curitems is None: return cmdline = map(str, self._targetcommand()) if cmdline == ['copy']: # import to shelf self.repo.thgshelves() # initialize repo.shelfdir 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 if self.p0chk.isChecked(): cmdline.append('-p0') cmdline.extend(['--verbose', '--']) cmdline.extend(map(hglib.tounicode, self.cslist.curitems)) sess = self._repoagent.runCommand(cmdline, self) self._cmdcontrol.setSession(sess) sess.commandFinished.connect(self._onCommandFinished) self._updateUi() @pyqtSlot(int) def _onCommandFinished(self, ret): self._updateUi() if ret == 0: self._runbutton.hide() self._cmdcontrol.setFocusToCloseButton() elif not self._cmdcontrol.session().isAborted(): cmdui.errorMessageBox(self._cmdcontrol.session(), self) if ret == 0 and not self._cmdcontrol.isLogVisible(): self._cmdcontrol.reject() def reject(self): self._cmdcontrol.reject() def _updateUi(self): self._runbutton.setEnabled(bool(self.getfilepaths()) and self._cmdcontrol.session().isFinished()) tortoisehg-4.5.2/tortoisehg/hgqt/csinfo.py0000644000175000017500000004437213242076403021523 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import binascii import re from .qtcore import ( QSize, Qt, pyqtSignal, ) from .qtgui import ( QHBoxLayout, QLabel, QWidget, ) from mercurial import error from ..util import hglib from ..util.i18n import _ from . import qtlib 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 try: if ctx.node() != repo.branchtip(ctx.branch()): return None except error.RepoLookupError: # ctx.branch() can be invalid for null or workingctx 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 ctx.thgtags() or None 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.instabilities() 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(str) def __init__(self, target, style, custom, repo, info): SummaryBase.__init__(self, target, custom, repo, info) QWidget.__init__(self) self.csstyle = style hbox = QHBoxLayout() hbox.setContentsMargins(0, 0, 0, 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.setContentsMargins(3, 3, 3, 3) margin.addWidget(self.expand_btn, 0, Qt.AlignTop) self.layout().insertLayout(0, margin) self.expand_btn.setVisible(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'(?:(?<=%%)|(? # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. from __future__ import absolute_import import cgi import os import re import tempfile import time from .qsci import ( QsciAPIs, ) from .qtcore import ( QSettings, QSize, QTimer, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAction, QActionGroup, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFont, QFrame, QHBoxLayout, QKeySequence, QLabel, QLineEdit, QMenu, QMessageBox, QPushButton, QShortcut, QSizePolicy, QStyle, QStyleFactory, QStyleOptionToolButton, QSplitter, QToolBar, QToolButton, QVBoxLayout, QWidget, ) from mercurial import ( error, obsolete, # delete if obsolete becomes enabled by default phases, util, ) from ..util import ( hglib, i18n, shlib, wconfig, ) from ..util.i18n import _ from . import ( branchop, cmdcore, cmdui, hgrcutil, lfprompt, qscilib, qtlib, revpanel, status, thgrepo, ) from .messageentry import MessageEntry if os.name == 'nt': from ..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) def mergecommitmessage(repo): wctx = repo[None] engmsg = repo.ui.configbool('tortoisehg', 'engmsg', False) 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()) return text 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 _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 _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(str) showMessage = pyqtSignal(str) grepRequested = pyqtSignal(str, dict) runCustomCommandRequested = pyqtSignal(str, list) commitComplete = pyqtSignal() progress = pyqtSignal(str, object, str, str, 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) self._repoagent = repoagent repo = repoagent.rawRepo() self._cmdsession = cmdcore.nullCmdSession() self._rev = rev self.lastAction = None # Dictionary storing the last (commit message, modified flag) # 'commit' is used for 'commit' and 'qnew', while # 'amend' is used for 'amend' and 'qrefresh' self.lastCommitMsgs = {'commit': ('', False), 'amend': ('', False)} 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.setSpacing(0) vbox.setContentsMargins(*(0,)*4) hbox = QHBoxLayout() 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.InstantPopup, toolTip=_('Copy one of the recent commit messages')) m = QMenu(self.recentMessagesButton) m.triggered.connect(self.msgSelected) self.recentMessagesButton.setMenu(m) tbar.addWidget(self.recentMessagesButton) self.updateRecentMessages() tbar.addAction(_('Options')).triggered.connect(self.details) tbar.setIconSize(qtlib.smallIconSize()) 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 = QLineEdit() if hasattr(self.pnedit, 'setPlaceholderText'): # Qt >= 4.7 self.pnedit.setPlaceholderText(_('### patch name ###')) 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 (obsolete._enabled or 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() pn = time.strftime('%Y-%m-%d_%H-%M-%S') pn += '_r%d+.diff' % self.repo['.'].rev() self.pnedit.setText(pn) 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[None].parents()) == 1 if curraction._name in ('qref', 'amend'): if self.lastAction not in ('qref', 'amend'): self.lastCommitMsgs['commit'] = (self.msgte.text(), self.msgte.isModified()) if self.lastCommitMsgs['amend'][0]: self.setMessage(*self.lastCommitMsgs['amend']) elif oldpctx is None or oldpctx.node() != pctx.node(): # pctx must be refreshed if hash changes self.setMessage(hglib.tounicode(pctx.description())) else: if self.lastAction in ('qref', 'amend'): self.lastCommitMsgs['amend'] = (self.msgte.text(), self.msgte.isModified()) self.setMessage(*self.lastCommitMsgs['commit']) elif len(self.repo[None].parents()) > 1: self.setMessage(mergecommitmessage(self.repo)) 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.branchmap(): # response: 0=Yes, 1=No, 2=Cancel if branch in [p.branch() for p in repo[None].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 = _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(str, str) 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[None].parents()) > 1: mode = 'merge' dlg = DetailsDialog(self._repoagent, 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(int) def repositoryChanged(self, flags): if flags & thgrepo.WorkingParentChanged: self._refreshWorkingState() elif flags & thgrepo.WorkingBranchChanged: self.refresh() def _refreshWorkingState(self): curraction = self.mqgroup.checkedAction() if curraction._name == 'commit' and not self.msgte.isModified(): # default merge or close-branch message is outdated if new commit # was made by other widget or process self.msgte.clear() self.lastCommitMsgs['amend'] = ('', False) # avoid loading stale cache # refresh() may load/save the stale 'amend' message in commitSetAction() self.refresh() self.stwidget.refreshWctx() # Trigger reload of working context # 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'] = ('', False) # clear saved stale cache @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 % cgi.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._repoagent, 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.msgte.setFocus() self.refresh() def canUndo(self): 'Returns undo description or None if not valid' desc, oldlen = hglib.readundodesc(self.repo) if desc == 'commit': return _('Rollback commit to revision %d') % (oldlen - 1) 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 = self.recentMessagesButton.menu() m.clear() for s in self.msghistory: title = s.split('\n', 1)[0][:70] a = m.addAction(title) a.setData(s) 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 @pyqtSlot(QAction) def msgSelected(self, action): 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 message = action.data() self.setMessage(message) self.msgte.setFocus() def setMessage(self, msg, modified=False): self.msgte.setText(msg) self.msgte.moveCursorToEnd() self.msgte.setModified(modified) 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 = hglib.shortrepoid(self.repo) lpref = prefix + '/commit/' # local settings (splitter, etc) gpref = 'commit/' # global settings (history, etc) # message history is stored in unicode self.split.restoreState(qtlib.readByteArray(s, lpref + 'split')) self.msgte.loadSettings(s, lpref+'msgte') self.stwidget.loadSettings(s, lpref+'status') self.msghistory = qtlib.readStringList(s, gpref + 'history-' + repoid) self.msghistory = [unicode(m) for m in self.msghistory if m] self.updateRecentMessages() self.userhist = qtlib.readStringList(s, gpref + 'userhist') self.userhist = [u for u in self.userhist if u] try: curmsg = self.repo.vfs('cur-message.txt').read() self.setMessage(hglib.tounicode(curmsg)) except EnvironmentError: pass try: curmsg = self.repo.vfs('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 = hglib.shortrepoid(self.repo) 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.vfs('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\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 Committed'), _('Please enter commit message'), parent=self) self.msgte.setFocus() return linkmandatory = self.repo.ui.configbool('tortoisehg', 'issue.linkmandatory', False) if linkmandatory: issueregex = None s = self.repo.ui.config('tortoisehg', 'issue.regex') if s: try: issueregex = re.compile(s) except re.error: pass if issueregex: m = issueregex.search(msg) if not m: qtlib.WarningMsgBox(_('Nothing Committed'), _('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[None].parents()) > 1: merge = True self.files = [] else: merge = False files = self.stwidget.getChecked('MAR?!IS') # 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 # username will be prompted as necessary by hg if ui.askusername user = self.opts.get('user') if not amend and not repo.ui.configbool('ui', 'askusername'): # TODO: no need to specify --user if it was read from ui user = qtlib.getCurrentUsername(self, self.repo, self.opts) if not user: return self.addUsernameToHistory(user) checkedUnknowns = self.stwidget.getChecked('?I') if checkedUnknowns and 'largefiles' in repo.extensions(): result = lfprompt.promptForLfiles(self, repo.ui, repo, checkedUnknowns) if not result: return checkedUnknowns, lfiles = result if lfiles: cmd = ['add', '--large', '--'] cmd.extend(map(hglib.escapepath, lfiles)) commandlines.append(cmd) 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: cmd = ['add', '--'] cmd.extend(map(hglib.escapepath, 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', '--'] cmd.extend(map(hglib.escapepath, checkedMissing)) commandlines.append(cmd) else: return cmdline = ['commit', '--verbose', '--message='+msg] if user: cmdline.extend(['--user', user]) date = self.opts.get('date') if date: cmdline += ['--date', date] cmdline += brcmd if partials: # 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(map(hglib.escapepath, self.files)) if len(repo[None].parents()) == 1: for fname in self.opts.get('autoinc', '').split(','): fname = fname.strip() if fname: cmdline.append('glob:%s' % 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'] = ('', False) 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() elif ret == 1 and self.currentAction in ('amend', 'commit'): qtlib.WarningMsgBox(_('Nothing Committed'), _('Nothing changed.'), parent=self) else: cmdui.errorMessageBox(self._cmdsession, self, _('Commit', 'window title')) class DetailsDialog(QDialog): 'Utility dialog for configuring uncommon settings' def __init__(self, repoagent, opts, userhistory, parent, mode='commit'): QDialog.__init__(self, parent) self.setWindowTitle(_('%s - commit options') % repoagent.displayName()) self._repoagent = repoagent 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) @property def repo(self): return self._repoagent.rawRepo() def saveInRepo(self): fn = os.path.join(self.repo.root, '.hg', 'hgrc') self.saveToPath([fn]) def saveGlobal(self): self.saveToPath(hglib.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._repoagent = repoagent self.pats = pats self.opts = opts layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 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(qtlib.readByteArray(s, 'commit/geom')) commit.loadSettings(s, 'committool') repoagent.repositoryChanged.connect(self.updateUndo) commit.commitComplete.connect(self.postcommit) self.setWindowTitle(_('%s - commit') % repoagent.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): repoagent = self._repoagent.subRepoAgent(uroot) 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-4.5.2/tortoisehg/hgqt/filelistview.py0000644000175000017500000000675113150123225022740 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( QModelIndex, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAbstractItemView, QTreeView, ) from ..util import hglib from . import qtlib class HgFileListView(QTreeView): """Display files and statuses between two revisions or patch""" fileSelected = pyqtSignal(str, str) clearDisplay = pyqtSignal() def __init__(self, parent): QTreeView.__init__(self, parent) self.setHeaderHidden(True) self.setDragDropMode(QAbstractItemView.DragOnly) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setRootIsDecorated(False) self.setTextElideMode(Qt.ElideLeft) # give consistent height and enable optimization self.setIconSize(qtlib.smallIconSize()) self.setUniformRowHeights(True) def setModel(self, model): QTreeView.setModel(self, model) model.layoutChanged.connect(self._onLayoutChanged) model.revLoaded.connect(self._onRevLoaded) self.selectionModel().currentRowChanged.connect(self._emitFileChanged) def currentFile(self): index = self.currentIndex() return hglib.fromunicode(self.model().filePath(index)) def setCurrentFile(self, path): model = self.model() model.fetchMore(QModelIndex()) # make sure path is populated self.setCurrentIndex(model.indexFromPath(hglib.tounicode(path))) def getSelectedFiles(self): model = self.model() return [hglib.fromunicode(model.filePath(index)) for index in self.selectedRows()] def _initCurrentIndex(self): m = self.model() if m.rowCount() > 0: self.setCurrentIndex(m.index(0, 0)) else: self.clearDisplay.emit() @pyqtSlot() def _onLayoutChanged(self): index = self.currentIndex() if index.isValid(): self.scrollTo(index) return self._initCurrentIndex() @pyqtSlot() def _onRevLoaded(self): index = self.currentIndex() if index.isValid(): # redisplay previous row self._emitFileChanged() else: self._initCurrentIndex() @pyqtSlot() def _emitFileChanged(self): index = self.currentIndex() m = self.model() if index.isValid(): # TODO: delete status from fileSelected because it isn't primitive # pseudo directory node has no status st = m.fileStatus(index) or '' self.fileSelected.emit(m.filePath(index), st) else: self.clearDisplay.emit() def selectedRows(self): return self.selectionModel().selectedRows() tortoisehg-4.5.2/tortoisehg/hgqt/postreview.ui0000644000175000017500000002674313150123225022431 0ustar sborhosborho00000000000000 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-4.5.2/tortoisehg/hgqt/htmldelegate.py0000644000175000017500000000360313150123225022662 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( QPointF, QSize, ) from .qtgui import ( QAbstractTextDocumentLayout, QPalette, QStyle, QStyleOptionViewItemV4, QStyledItemDelegate, QTextDocument, ) class HTMLDelegate(QStyledItemDelegate): def 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): doc = QTextDocument(defaultFont=option.font) doc.setHtml(index.data()) return doc tortoisehg-4.5.2/tortoisehg/hgqt/graphopt.py0000644000175000017500000004006713251112733022060 0ustar sborhosborho00000000000000# Copyright (c) 2016 Unity Technologies. # # 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. """helper functions for efficiently computing graphs for the revision history This module provides an optimised model for computing graph layouts that are identical to, but with different colored edges to the normal graph layout algorithm. The optimised model is both faster to compute and uses less memory for repositories with a lot of history. """ import itertools from collections import defaultdict from mercurial import revset from mercurial import util from tortoisehg.util import obsoleteutil from tortoisehg.hgqt import graph as graphmod from tortoisehg.hgqt.graph import ( LINE_TYPE_PARENT, LINE_TYPE_FAMILY, LINE_TYPE_OBSOLETE, ) def _compute_lines(rev, prevs, revs, active_edges): """Computes current index and next line's index of each active edge Args: active_edges (list[GraphEdge]): Edges relevant for the current line. rev (int): The current revision. prevs (list[int]): Index of nodes for the previous line. revs (list[int]): Index of nodes for the current line. """ lines = [] for edge in active_edges: if edge.startrev == rev: start_rev = edge.startrev else: start_rev = edge.endrev pos = (prevs.index(start_rev), revs.index(edge.endrev)) lines.append((pos, edge)) return lines def _create_node(repo, rev, index_info): ctx = repo[rev] xposition, prevs, revs, actedges = index_info lines = _compute_lines(rev, prevs, revs, actedges) return graphmod.GraphNode.fromchangectx(repo, ctx, xposition, lines) class GraphEdge(object): """Edge from startrev to endrev""" def __init__(self, graph, startrev, endrev, linktype): self.graph = graph self.startrev = startrev self.endrev = endrev self.linktype = linktype self._color = None @property def color(self): """Color of the edge, uses branch color of endrev""" if self._color is None: self._color = self.graph.edge_color( self.graph.repo[self.endrev].branch()) return self._color def __repr__(self): return '%s(%r->%r, color=%r, linetype=%r)' % ( self.__class__.__name__, self.startrev, self.endrev, self.color, self.linktype) @property def importance(self): """Sort key of overlapped edges; highest one should be drawn last""" # prefer parent-child relation and younger (i.e. longer) edge return -self.linktype, -self.endrev def _branch_spec(repo, branch, all_parents): if all_parents: return repo.revs('::branch(%s)', branch) else: return repo.revs('branch(%s)', branch) class Graph(object): """Efficient graph layouter for repositories. The grapher pre-computes the overall layout of the graph in a memory and time efficient way, caching enough data that the current view can be calculated efficiently without putting an unreasonable amount of strain on system memory usage for large repositories. Currently, it is possible to visualise a few hundred nodes and keep the layout of a 200k+ changeset repository in about 2.4 GB memory, compared to about 14 GB memory for the existing graph layout algorithm. To achieve efficiency, this layouter does not support drawing graft or obsoletion edges.""" def __init__(self, repo, opts): self._repo = repo self._graph = {} self._cache = util.lrucachedict(1000) self._revset_set = opts.get('revset', set()) self._revset_set_pure = self._revset_set self._revset = sorted(self._revset_set, reverse=True) self._all_parents = opts.get('allparents', False) self._show_family_line = opts.get('showfamilyline', False) self._show_graft_source = opts.get('showgraftsource', False) branch = opts.get('branch', '') if branch: revs = _branch_spec(self._repo, branch, self._all_parents) add_none = False if self._revset: self._revset_set = self._revset_set & frozenset(revs) else: self._revset_set = frozenset(revs) prevs = set([pctx.rev() for pctx in self._repo[None].parents()]) if prevs & self._revset_set: add_none = True self._revset = sorted(self._revset_set, reverse=True) if add_none: self._revset.insert(0, None) self._revset_set_pure = self._revset_set self._revset_set = frozenset(self._revset_set | set([None])) self._edge_color_cache = {} self._grapher = self._build_nodes() self._row_to_rev = dict( enumerate(self._get_revision_iterator())) @property def _clean_revset_set(self): if self._revset_set_pure: return self._revset_set_pure return self._revset_set def edge_color(self, branch): """Color function for edges""" idx = self._edge_color_cache.get(branch, None) if idx is None: idx = graphmod.hashcolor(branch) self._edge_color_cache[branch] = idx return idx def isfilled(self): """Indicates whether the graph is done computing. We prefer to cheat the repository model into recreating rather than letting it try to overwrite parts of the model as it does not fit with the normal layouting model. This value is thus always false.""" return False @property def repo(self): """Repository instance for the graph""" return self._repo def _workingdir_parents(self): parents = [ctx.rev() for ctx in self._repo[None].parents()] if len(parents) == 1: parents.append(-1) return parents def _get_revision_iterator(self): if self._revset: revs = revset.spanset(self._repo, max(self._clean_revset_set), min(self._clean_revset_set) - 1) if None not in self._revset_set: return revs return itertools.chain([None], revs) else: revs = revset.spanset(self._repo, len(self._repo) - 1, -1) return itertools.chain([None], revs) def _filter_parents(self, p1, p2): """omit parents not in revset as well as degenerate repository cases""" if self._revset: if p1 not in self._revset_set: p1 = -1 if p2 not in self._revset_set: p2 = -1 if p1 == p2: p2 = -1 return p1, p2 def _find_family(self, family, rev, op, p, op2, p2): if not self._revset: return [p, p2] include_p1 = op != -1 and p == -1 include_p2 = op2 != -1 and p2 == -1 if not include_p1 and not include_p2: return [p, p2] rv = [] if not include_p1: rv.append(p) if not include_p2: rv.append(p2) fam = family[rev] candidates = [c for c, cisp1 in fam if include_p1 and cisp1 or include_p2 and not cisp1] if not candidates: return rv if len(candidates) == 1: return rv + candidates remove = set() for candidate in candidates: remove |= set([c for c, _ in family[candidate]]) return sorted(rv + list(set(candidates) - remove), reverse=True) def _pre_compute_family(self, parentrevs): """ Calculates the ancestry of all involved changesets if we're displaying a revset and showing family lines are enabled. A family line is added to an edge if there is no direct parent connected to it and there is an ancestral relationship between the two. If the ancestor can be reached both through the first and second parent, only the first parent ancestor edge is rendered. Args: parentrevs (dict[int,list[int]]): Parents for each revision. """ if not self._revset or not self._show_family_line: return None anc = defaultdict(set) holders = defaultdict(set) revrange = self._get_revision_iterator() for r in revrange: p1, p2 = parentrevs[r] toupdate = holders.pop(r, ()) if not toupdate and r not in self._revset_set: continue for origrev, currentrev, isp1, ndup in toupdate: if not ndup: if p1 != -1 and p1 in self._revset_set: anc[origrev].add((p1, isp1)) if p2 != -1 and p2 in self._revset_set: anc[origrev].add((p2, isp1)) if p1 != -1: if p1 not in self._revset_set: holders[p1].add((origrev, p1, isp1, ndup)) else: anc[origrev].add((p1, isp1)) if p2 != -1: if p2 not in self._revset_set: holders[p2].add((origrev, p2, isp1, ndup)) else: anc[origrev].add((p2, isp1)) if r in self._revset_set: if p1 != -1: holders[p1].add((r, p1, True, p1 not in self._revset_set)) if p1 in self._revset_set: anc[r].add((p1, True)) if p2 != -1: holders[p2].add((r, p2, False, p2 not in self._revset_set)) if p2 in self._revset_set: anc[r].add((p2, False)) continue return anc def _add_obsolete(self, rev, parents_to_add, actedge): """Resolves obsolete edges. This is a mangled copy from obsoleteutil.first_known_precursors that avoids using context lookups, except to determine filtering state. """ if self._revset and rev not in self._revset_set: return revs = list(obsoleteutil.first_known_precursors_rev(self._repo, rev)) if self._revset: revs = [r for r in revs if r in self._revset_set] for r in revs: actedge[r].append(GraphEdge(self, rev, r, LINE_TYPE_OBSOLETE)) parents_to_add.append(r) def _build_nodes(self): """ Generator for computing necessary information to layout the graph. For each revision the previous node indexes are computed, the current node indexes, as well as the active edges that should be rendered. Returns: Revision just processed. """ clog = self._repo.changelog parentrevs = dict([(r, clog.parentrevs(r)) for r in clog]) parentrevs[None] = self._workingdir_parents() actedge = defaultdict(list) revs = [] revrange = self._get_revision_iterator() family = self._pre_compute_family(parentrevs) for rev in revrange: addparents = not self._revset_set or rev in self._revset_set if rev not in revs and addparents: revs.append(rev) rev_index = revs.index(rev) if addparents else 0 if rev in actedge: del actedge[rev] op1, op2 = parentrevs[rev] p1, p2 = self._filter_parents(op1, op2) p1l, p2l = LINE_TYPE_PARENT, LINE_TYPE_PARENT # compute family lines if enabled and we're in revset mode if self._revset and addparents and self._show_family_line: fp1, fp2 = p1, p2 parents = self._find_family(family, rev, op1, p1, op2, p2) if addparents: for p in parents: if p != -1: pl = LINE_TYPE_FAMILY if p != fp1 and p != fp2 else p1l actedge[p].append(GraphEdge(self, rev, p, pl)) parents_to_add = [p for p in parents if p != -1 and p not in revs] else: if p1 != -1 and addparents: actedge[p1].append(GraphEdge(self, rev, p1, p1l)) if p2 != -1 and addparents: actedge[p2].append(GraphEdge(self, rev, p2, p2l)) parents_to_add = [p for p in (p1, p2) if p != -1 and p not in revs] if self._show_graft_source: self._add_obsolete(rev, parents_to_add, actedge) prevs = revs[:] if addparents: revs[rev_index:rev_index + 1] = parents_to_add self._graph[rev] = ( rev_index, prevs, revs[:], list(itertools.chain(*actedge.values()))) yield rev def build_nodes(self, fillstep=None, rev=None): """Ensures that the graph layout is computed to the specified point. If fillstep is specified, it is the number of revisions to fetch from the current point. If rev is specified, it is the revision that we must load until. """ if self._grapher is None: return if rev is not None: if rev not in self._graph: while True: r = next(self._grapher, -1) if r == -1: self._grapher = None break if r < rev and r is not None: break if fillstep is not None: if self._grapher: for i in xrange(0, fillstep): if next(self._grapher, -1) == -1: self._grapher = None break def __len__(self): """Returns the number of revisions in the graph. If we are not in revset mode, this also includes an extra revision for the working context. """ if self._revset: return len(self._revset) return len(self._row_to_rev) def _get_or_load_graph_node(self, rev): """Retrieve node from graph cache. This will load graph layout information until rev is reached if it is not already available, potentially blocking for a while. """ if rev not in self._graph: self.build_nodes(rev=rev) return self._graph[rev] def _rev_from_row(self, row): if self._revset: if row < 0 or row >= len(self._revset): return self._revset[-1] return self._revset[row] return self._row_to_rev[row] def __getitem__(self, row): """Entry point for repomodel to fetch individual rows for display. Once the overall layout is determined, a node is either fetched from the internal LRU cache, or instantiated, which will trigger the final computation of edge layout of the row. """ rev = self._rev_from_row(row) if rev in self._cache: node = self._cache[rev] if node is not None: return node idxinfo = self._get_or_load_graph_node(rev) node = _create_node(self._repo, rev, idxinfo) if row > 0: prevrev = self._rev_from_row(row - 1) if (not self._revset or None in self._revset) or prevrev is not None: pidxinfo = self._get_or_load_graph_node(prevrev) _pprevs, _prevs, _pactedges = pidxinfo[1:] plines = _compute_lines(prevrev, _pprevs, _prevs, _pactedges) node.toplines = plines[:] self._cache[rev] = node return node def index(self, rev): """Get row number for specified revision""" idx = 0 isrevset = bool(self._revset) for iter_rev in self._get_revision_iterator(): if iter_rev == rev: return idx if not isrevset or iter_rev in self._revset_set: idx += 1 raise ValueError('rev %r not found' % rev) tortoisehg-4.5.2/tortoisehg/hgqt/clone.py0000644000175000017500000003424313242076403021336 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os from .qtcore import ( QDir, QTimer, pyqtSignal, pyqtSlot, ) from .qtgui import ( QCheckBox, QComboBox, QFileDialog, QFormLayout, QHBoxLayout, QLineEdit, QPushButton, QSizePolicy, QVBoxLayout, QWidget, ) from mercurial import ( cmdutil, commands, hg, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, qtlib, ) def _startrev_available(): entry = cmdutil.findcmd('clone', commands.table)[1] longopts = set(e[1] for e in entry[1]) return 'startrev' in longopts def _suggesteddest(src, basedest): if '://' in basedest: return basedest try: if not os.listdir(basedest): # premade empty directory, just use it return basedest except OSError: # guess existing base assuming "{basedest}/{name}" basedest = os.path.dirname(basedest) name = hglib.tounicode(hg.defaultdest(hglib.fromunicode(src, 'replace'))) if not name or name == '.': return basedest newdest = os.path.join(basedest, name) if os.path.exists(newdest): newdest += '-clone' return newdest class CloneWidget(cmdui.AbstractCmdWidget): def __init__(self, ui, cmdagent, args=None, opts={}, parent=None): super(CloneWidget, self).__init__(parent) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self._cmdagent = cmdagent self.ui = ui dest = src = 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) ## main layout form = QFormLayout() form.setContentsMargins(0, 0, 0, 0) self.setLayout(form) ### source combo and button self.src_combo = QComboBox() self.src_combo.setEditable(True) self.src_combo.setMinimumContentsLength(30) # cut long path self.src_btn = QPushButton(_('Browse...')) self.src_btn.setAutoDefault(False) self.src_btn.clicked.connect(self._browseSource) srcbox = QHBoxLayout() srcbox.addWidget(self.src_combo, 1) srcbox.addWidget(self.src_btn) form.addRow(_('Source:'), srcbox) ### destination combo and button self.dest_combo = QComboBox() self.dest_combo.setEditable(True) self.dest_combo.setMinimumContentsLength(30) # cut long path self.dest_btn = QPushButton(_('Browse...')) self.dest_btn.setAutoDefault(False) self.dest_btn.clicked.connect(self._browseDestination) destbox = QHBoxLayout() destbox.addWidget(self.dest_combo, 1) destbox.addWidget(self.dest_btn) form.addRow(_('Destination:'), destbox) for combo in (self.src_combo, self.dest_combo): qtlib.allowCaseChangingInput(combo) combo.installEventFilter(qtlib.BadCompletionBlocker(combo)) self.setSource(usrc) self.setDestination(udest) ### options expander = qtlib.ExpanderLabel(_('Options'), False) optwidget = QWidget(self) expander.expanded.connect(optwidget.setVisible) optbox = QVBoxLayout() optbox.setContentsMargins(0, 0, 0, 0) optbox.setSpacing(6) optwidget.setLayout(optbox) form.addRow(expander, optwidget) 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.rev_text.setToolTip(_('A revision identifier, bookmark, tag or ' 'branch name')) self.noupdate_chk = QCheckBox(_('Do not update the new working directory')) self.pproto_chk = QCheckBox(_('Use pull protocol to copy metadata')) self.stream_chk = QCheckBox(_('Clone with minimal processing')) optbox.addWidget(self.noupdate_chk) optbox.addWidget(self.pproto_chk) optbox.addWidget(self.stream_chk) self.qclone_chk, self.qclone_txt, self.qclone_btn = \ chktext(_('Include patch queue'), btnlabel=_('Browse...'), btnslot=self._browsePatchQueue) 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:')) self.largefiles_chk = QCheckBox(_('Use largefiles')) optbox.addWidget(self.largefiles_chk) # allow to specify start revision for p4 & svn repos. self.startrev_chk, self.startrev_text = chktext(_('Start revision:'), stretch=40) self.hgcmd_txt = QLineEdit() self.hgcmd_txt.setReadOnly(True) form.addRow(_('Hg command:'), self.hgcmd_txt) # connect extra signals self.src_combo.editTextChanged.connect(self._onSourceChanged) self.src_combo.currentIndexChanged.connect(self._suggestDestination) t = QTimer(self, interval=200, singleShot=True) t.timeout.connect(self._suggestDestination) le = self.src_combo.lineEdit() le.editingFinished.connect(t.stop) # only while it has focus le.textEdited.connect(t.start) 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.stream_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.largefiles_chk.toggled.connect(self._composeCommand) self.startrev_chk.toggled.connect(self._composeCommand) # prepare to show optwidget.hide() 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.stream_chk.setChecked(bool(opts.get('stream'))) self.startrev_chk.setVisible(_startrev_available()) self.startrev_text.setVisible(_startrev_available()) self._composeCommand() def readSettings(self, qs): for key, combo in [('source', self.src_combo), ('dest', self.dest_combo)]: # addItems() can overwrite temporary edit text edittext = combo.currentText() combo.blockSignals(True) combo.addItems(qtlib.readStringList(qs, key)) combo.setCurrentIndex(combo.findText(edittext)) combo.setEditText(edittext) combo.blockSignals(False) self.src_combo.lineEdit().selectAll() def writeSettings(self, qs): for key, combo in [('source', self.src_combo), ('dest', self.dest_combo)]: l = [combo.currentText()] l.extend(combo.itemText(i) for i in xrange(combo.count()) if combo.itemText(i) != combo.currentText()) qs.setValue(key, l[:10]) def source(self): return unicode(self.src_combo.currentText()).strip() def setSource(self, url): self.src_combo.setCurrentIndex(self.src_combo.findText(url)) self.src_combo.setEditText(url) def destination(self): return unicode(self.dest_combo.currentText()).strip() def setDestination(self, url): self.dest_combo.setCurrentIndex(self.dest_combo.findText(url)) self.dest_combo.setEditText(url) @pyqtSlot() def _suggestDestination(self): self.setDestination(_suggesteddest(self.source(), self.destination())) @pyqtSlot() def _composeCommand(self): opts = { 'noupdate': self.noupdate_chk.isChecked(), 'stream': self.stream_chk.isChecked(), 'pull': self.pproto_chk.isChecked(), 'verbose': True, 'config': [], } if (self.ui.config('http_proxy', 'host') and not self.proxy_chk.isChecked()): opts['config'].append('http_proxy.host=') if self.remote_chk.isChecked(): opts['remotecmd'] = unicode(self.remote_text.text()).strip() or None if self.rev_chk.isChecked(): opts['rev'] = unicode(self.rev_text.text()).strip() or None if self.startrev_chk.isChecked(): opts['startrev'] = (unicode(self.startrev_text.text()).strip() or None) if self.largefiles_chk.isChecked(): opts['config'].append('extensions.largefiles=') src = self.source() dest = self.destination() if src.startswith('https://'): opts['insecure'] = self.insecure_chk.isChecked() if self.qclone_chk.isChecked(): name = 'qclone' opts['patches'] = unicode(self.qclone_txt.text()).strip() or None else: name = 'clone' cmdline = hglib.buildcmdargs(name, src, dest or None, **opts) self.hgcmd_txt.setText('hg ' + hglib.prettifycmdline(cmdline)) self.commandChanged.emit() return cmdline def canRunCommand(self): src, dest = self.source(), self.destination() return bool(src and dest and src != dest) def runCommand(self): cmdline = self._composeCommand() return self._cmdagent.runCommand(cmdline, self) @pyqtSlot() def _browseSource(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._suggestDestination() self.dest_combo.setFocus() @pyqtSlot() def _browseDestination(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._suggestDestination() # in case existing dir is selected self.dest_combo.setFocus() @pyqtSlot() def _browsePatchQueue(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() @pyqtSlot() def _onSourceChanged(self): self.insecure_chk.setEnabled(self.source().startswith('https://')) self._composeCommand() class CloneDialog(cmdui.CmdControlDialog): clonedRepository = pyqtSignal(str, str) def __init__(self, ui, args=None, opts={}, parent=None): super(CloneDialog, self).__init__(parent) cwd = os.getcwd() ucwd = hglib.tounicode(cwd) self.setWindowTitle(_('Clone - %s') % ucwd) self.setWindowIcon(qtlib.geticon('hg-clone')) self.setObjectName('clone') self.setRunButtonText(_('&Clone')) self._cmdagent = cmdagent = cmdcore.CmdAgent(ui, self) cmdagent.serviceStopped.connect(self.reject) self.setCommandWidget(CloneWidget(ui, cmdagent, args, opts, self)) self.commandFinished.connect(self._emitCloned) def source(self): return self.commandWidget().source() def setSource(self, url): assert self.isCommandFinished() self.commandWidget().setSource(url) def destination(self): return self.commandWidget().destination() def setDestination(self, url): assert self.isCommandFinished() self.commandWidget().setDestination(url) @pyqtSlot(int) def _emitCloned(self, ret): if ret == 0: self.clonedRepository.emit(self.destination(), self.source()) def done(self, r): if self._cmdagent.isServiceRunning(): self._cmdagent.stopService() return # postponed until serviceStopped super(CloneDialog, self).done(r) tortoisehg-4.5.2/tortoisehg/hgqt/bookmark.py0000644000175000017500000004226013150123225022032 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import import re from .qtcore import ( QPoint, Qt, pyqtSlot, ) from .qtgui import ( QAction, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFormLayout, QFrame, QHBoxLayout, QLabel, QLayout, QListWidget, QLineEdit, QMenu, QSizePolicy, QSplitter, QVBoxLayout, QWidget, ) from ..util import hglib from ..util.i18n import _ from . 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.SetMinAndMaxSize) self.setLayout(base) ## main layout grid formwidget = QWidget(self) formwidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) form = QFormLayout(fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow) formwidget.setLayout(form) base.addWidget(formwidget) form.addRow(_('Revision:'), QLabel('%d (%s)' % (rev, repo[rev]))) ### bookmark combo self.bookmarkCombo = QComboBox() self.bookmarkCombo.setEditable(True) self.bookmarkCombo.setMinimumContentsLength(30) # cut long name 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) form.addRow(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') % repoagent.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 hglib.activebookmark(self.repo) in cs_bookmarks: bm = hglib.tounicode(hglib.activebookmark(self.repo)) 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.setVisible(True) self.sep.setVisible(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) _extractbookmarknames = re.compile(r'(.*) [0-9a-f]{12,}$', re.MULTILINE).findall class SyncBookmarkDialog(QDialog): def __init__(self, repoagent, syncurl=None, parent=None): QDialog.__init__(self, parent) self._repoagent = repoagent self._syncurl = syncurl self._cmdsession = cmdcore.nullCmdSession() self._insess = cmdcore.nullCmdSession() self._outsess = cmdcore.nullCmdSession() self.setWindowTitle(_('TortoiseHg Bookmark Sync')) self.setWindowIcon(qtlib.geticon('thg-sync-bookmarks')) base = QVBoxLayout() base.setSpacing(0) base.setContentsMargins(2, 2, 2, 2) self.setLayout(base) # horizontal splitter self.splitter = QSplitter(self) self.splitter.setOrientation(Qt.Horizontal) self.splitter.setChildrenCollapsible(False) self.splitter.setObjectName('splitter') self.layout().addWidget(self.splitter) # outgoing frame outgoingFrame = QFrame(self.splitter) outgoingLayout = QVBoxLayout() outgoingLayout.setSpacing(2) outgoingLayout.setContentsMargins(2, 2, 2, 2) outgoingFrame.setLayout(outgoingLayout) outgoingLabel = QLabel(_('Outgoing Bookmarks')) outgoingLayout.addWidget(outgoingLabel) self.outgoingList = QListWidget(self) self.outgoingList.setContextMenuPolicy(Qt.CustomContextMenu) self.outgoingList.setSelectionMode(QListWidget.ExtendedSelection) self.outgoingList.customContextMenuRequested.connect( self._onOutgoingMenuRequested) self.outgoingList.itemSelectionChanged.connect(self._updateActions) outgoingLayout.addWidget(self.outgoingList) self._outactions = [] a = QAction(_('&Push Bookmark'), self) a.triggered.connect(self.push_bookmark) self._outactions.append(a) a = QAction(_('&Remove Bookmark'), self) a.triggered.connect(self.remove_outgoing) self._outactions.append(a) self.addActions(self._outactions) outgoingBtnLayout = QHBoxLayout() outgoingBtnLayout.setSpacing(2) outgoingBtnLayout.setContentsMargins(2, 2, 2, 2) for a in self._outactions: outgoingBtnLayout.addWidget(qtlib.ActionPushButton(a, self)) outgoingLayout.addLayout(outgoingBtnLayout) # incoming frame incomingFrame = QFrame(self.splitter) incomingLayout = QVBoxLayout() incomingLayout.setSpacing(2) incomingLayout.setContentsMargins(2, 2, 2, 2) incomingFrame.setLayout(incomingLayout) incomingLabel = QLabel(_('Incoming Bookmarks')) incomingLayout.addWidget(incomingLabel) self.incomingList = QListWidget(self) self.incomingList.setContextMenuPolicy(Qt.CustomContextMenu) self.incomingList.setSelectionMode(QListWidget.ExtendedSelection) self.incomingList.customContextMenuRequested.connect( self._onIncomingMenuRequested) self.incomingList.itemSelectionChanged.connect(self._updateActions) incomingLayout.addWidget(self.incomingList) self._inactions = [] a = QAction(_('P&ull Bookmark'), self) a.triggered.connect(self.pull_bookmark) self._inactions.append(a) a = QAction(_('R&emove Bookmark'), self) a.triggered.connect(self.remove_incoming) self._inactions.append(a) self.addActions(self._inactions) incomingBtnLayout = QHBoxLayout() incomingBtnLayout.setSpacing(2) incomingBtnLayout.setContentsMargins(2, 2, 2, 2) for a in self._inactions: incomingBtnLayout.addWidget(qtlib.ActionPushButton(a, self)) incomingLayout.addLayout(incomingBtnLayout) # status line self.status = qtlib.StatusLabel() self.status.setContentsMargins(4, 2, 4, 4) self.layout().addWidget(self.status) self._finishmsg = None self.refresh() def set_status(self, text, icon=None): self.status.set_status(text, icon) @pyqtSlot() def refresh(self): """ update the bookmark lists """ cmdline = hglib.buildcmdargs('outgoing', self._syncurl, bookmarks=True) self._outsess = sess = self._repoagent.runCommand(cmdline, self) sess.setCaptureOutput(True) sess.commandFinished.connect(self._onListLocalBookmarksFinished) cmdline = hglib.buildcmdargs('incoming', self._syncurl, bookmarks=True) self._insess = sess = self._repoagent.runCommand(cmdline, self) sess.setCaptureOutput(True) sess.commandFinished.connect(self._onListRemoteBookmarksFinished) self._updateActions() @pyqtSlot() def _onListLocalBookmarksFinished(self): self._onListBookmarksFinished(self._outsess, self.outgoingList) @pyqtSlot() def _onListRemoteBookmarksFinished(self): self._onListBookmarksFinished(self._insess, self.incomingList) def _onListBookmarksFinished(self, sess, worklist): ret = sess.exitCode() if ret == 0: bookmarks = _extractbookmarknames(str(sess.readAll())) self._updateBookmarkList(worklist, bookmarks) elif ret == 1: self._updateBookmarkList(worklist, []) else: self.set_status(sess.errorString(), False) self._updateActions() def selectedOutgoingBookmarks(self): return [unicode(x.text()) for x in self.outgoingList.selectedItems()] def selectedIncomingBookmarks(self): return [unicode(x.text()) for x in self.incomingList.selectedItems()] @pyqtSlot() def push_bookmark(self): self._sync('push', self.selectedOutgoingBookmarks(), _('Pushed local bookmark: %s')) @pyqtSlot() def pull_bookmark(self): self._sync('pull', self.selectedIncomingBookmarks(), _('Pulled remote bookmark: %s')) @pyqtSlot() def remove_incoming(self): self._sync('push', self.selectedIncomingBookmarks(), _('Removed remote bookmark: %s')) def _sync(self, cmdname, selected, finishmsg): if not selected: return self._finishmsg = finishmsg % ', '.join(selected) cmdline = hglib.buildcmdargs(cmdname, self._syncurl, bookmark=selected) self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self._onBoomarkHandlingFinished) self._updateActions() @pyqtSlot() def remove_outgoing(self): selected = self.selectedOutgoingBookmarks() if not selected: return self._finishmsg = _('Removed local bookmark: %s') % ', '.join(selected) cmdline = hglib.buildcmdargs('bookmark', *selected, delete=True) self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self._onBoomarkHandlingFinished) self._updateActions() @pyqtSlot(int) def _onBoomarkHandlingFinished(self, ret): if ret == 0 or ret == 1: self.set_status(self._finishmsg, True) else: self.set_status(self._cmdsession.errorString(), False) self.refresh() def _updateBookmarkList(self, worklist, bookmarks): selected = [x.text() for x in worklist.selectedItems()] worklist.clear() bookmarks = [hglib.tounicode(x.strip()) for x in bookmarks] worklist.addItems(bookmarks) for select in selected: items = worklist.findItems(select, Qt.MatchExactly) for item in items: item.setSelected(True) @pyqtSlot(QPoint) def _onOutgoingMenuRequested(self, pos): self._popupMenuFor(self._outactions, self.outgoingList, pos) @pyqtSlot(QPoint) def _onIncomingMenuRequested(self, pos): self._popupMenuFor(self._inactions, self.incomingList, pos) def _popupMenuFor(self, actions, worklist, pos): menu = QMenu(self) menu.addActions(actions) menu.setAttribute(Qt.WA_DeleteOnClose) menu.popup(worklist.viewport().mapToGlobal(pos)) @pyqtSlot() def _updateActions(self): state = all(sess.isFinished() for sess in [self._cmdsession, self._insess, self._outsess]) for a in self._outactions: a.setEnabled(state and bool(self.selectedOutgoingBookmarks())) for a in self._inactions: a.setEnabled(state and bool(self.selectedIncomingBookmarks())) tortoisehg-4.5.2/tortoisehg/hgqt/repotreemodel.py0000644000175000017500000003472213150123225023077 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import import os from .qtcore import ( QAbstractItemModel, QByteArray, QFile, QIODevice, QMimeData, QModelIndex, QUrl, QXmlStreamReader, QXmlStreamWriter, Qt, pyqtSlot, ) from .qtgui import ( QFont, ) from ..util import hglib from ..util.i18n import _ from . import repotreeitem 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()) if ele != rootElementName: print "unexpected xml element '%s' "\ "(was looking for %s)" % (ele, rootElementName) return if xr.hasError(): print hglib.fromunicode(xr.errorString(), 'replace') if xr.readNextStartElement(): itemread = repotreeitem.undumpObject(xr) xr.skipCurrentElement() if xr.hasError(): print hglib.fromunicode(xr.errorString(), 'replace') 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 https://doc.qt.io/qt-4.8/model-view-programming.html#model-subclassing-reference # 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 None if role not in (Qt.DisplayRole, Qt.EditRole, Qt.DecorationRole, Qt.FontRole): return None 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 _('Path') return None 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 [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(item.rootpath())]) else: d.setText(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 if not value: 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 uroot = os.path.normpath(uroot) # Check whether the repo that we are adding is a subrepo knownitem = self.getRepoItem(uroot, lookForSubrepos=True) itemIsSubrepo = isinstance(knownitem, (repotreeitem.StandaloneSubrepoItem, repotreeitem.SubrepoItem)) self.beginInsertRows(parent, row, row) if itemIsSubrepo: ri = repotreeitem.StandaloneSubrepoItem(uroot) else: ri = repotreeitem.RepoItem(uroot) 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(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 and item is not self.rootItem: 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 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 itemPath(self, index): """Virtual path of the item at the given index""" if index.isValid(): item = index.internalPointer() else: item = self.rootItem return repotreeitem.itempath(item) def indexFromItemPath(self, path, column=0): """Model index for the item specified by the given virtual path""" try: item = repotreeitem.findbyitempath(self.rootItem, unicode(path)) except ValueError: return QModelIndex() return self._indexFromItem(item, column) 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) # TODO: rename loadSubrepos() and appendSubrepos() to scanRepo() ? 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._sharedpath != tmpitem._sharedpath or item._valid != tmpitem._valid): item._sharedpath = tmpitem._sharedpath item._valid = tmpitem._valid self._emitItemDataChanged(item) return map(hglib.tounicode, invalidpaths) def updateCommonPaths(self, showShortPaths=None): if showShortPaths is not None: self.showShortPaths = showShortPaths for grp in self.rootItem.childs: if isinstance(grp, repotreeitem.RepoGroupItem): if self.showShortPaths: grp.updateCommonPath() else: grp.updateCommonPath('') @pyqtSlot(str) def _updateShortName(self, uroot): uroot = unicode(uroot) repoagent = self._repomanager.repoAgent(uroot) it = self.getRepoItem(uroot) if it: it.setShortName(repoagent.shortName()) self._emitItemDataChanged(it) @pyqtSlot(str) def _updateBaseNode(self, uroot): uroot = unicode(uroot) repo = self._repomanager.repoAgent(uroot).rawRepo() it = self.getRepoItem(uroot) if it: it.setBaseNode(hglib.repoidnode(repo)) @pyqtSlot(str) 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-4.5.2/tortoisehg/hgqt/pbranch.py0000644000175000017500000007427113150123225021651 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import errno import os from .qtcore import ( QAbstractTableModel, QPointF, QRectF, Qt, pyqtSlot, ) from .qtgui import ( QAbstractItemView, QCheckBox, QColor, QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QMenu, QPainter, QPainterPath, QPen, QPixmap, QPolygonF, QSizePolicy, QSplitter, QStackedWidget, QTableView, QToolBar, QVBoxLayout, QWidget, QWidgetAction, ) from mercurial import ( error, extensions, util, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, qtlib, revdetails, update, ) from .qtlib import geticon PATCHCACHEPATH = 'thgpbcache' nullvariant = None 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.refresh) # 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.setIconSize(qtlib.toolBarIconSize()) 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("hg-add")) #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() @pyqtSlot() 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.pbranch.cmdnew(self.repo.ui, self.repo, patch_name) self._repoagent.pollStatus() 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)) 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.vfs.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.vfs.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[None].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()) 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 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, unicode): text = hglib.tounicode(text) return 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 self._headers[section] if role == Qt.TextAlignmentRole: return 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 pix class PNewDialog(QDialog): def __init__(self, parent=None): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.setWindowIcon(qtlib.geticon("hg-add")) 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-4.5.2/tortoisehg/hgqt/messageentry.py0000644000175000017500000002261513153775104022751 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import re from .qsci import ( QsciScintilla, QsciLexerMakefile, ) from .qtcore import ( QPoint, QSettings, Qt, pyqtSlot, ) from .qtgui import ( QColor, QFontMetrics, ) from ..util.i18n import _ from . import ( qscilib, qtlib, ) 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) # https://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 qtlib.readBool(QSettings(), 'msgentry/lexer', True): 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 = unicode(self.text()).splitlines() 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 = lines[b].strip() if istopboundary(linetext) or isanyboundary(linetext): break if b >= 1: nextlinetext = lines[b - 1].strip() if isbottomboundary(nextlinetext) \ or isanyboundary(nextlinetext): break b -= 1 e = line while e+1 < len(lines) and len(lines[e+1]) > 1: linetext = lines[e].strip() if isbottomboundary(linetext) or isanyboundary(linetext): break nextlinetext = lines[e + 1].strip() 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 l = group[curlinenum - b] group[curlinenum - b] = l[:curcol] + MARKER + l[curcol:] firstlinetext = lines[b] if firstlinetext: indentcount = len(firstlinetext) - len(firstlinetext.lstrip()) firstindent = firstlinetext[:indentcount] else: indentcount = 0 firstindent = '' parts = [] for l in group: parts.extend(l.split()) outlines = [] line = [] partslen = indentcount - 1 newcurlinenum, 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(' '.join(line)) + 1 + part.index(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 = ' '.join(line) if len(outlines) == 0 and firstindent: linetext = firstindent + linetext outlines.append(linetext) line, partslen = [], 0 line.append(part) partslen += len(part) if line: outlines.append(' '.join(line)) self.beginUndoAction() self.setSelection(b, 0, e+1, 0) self.removeSelectedText() self.insertAt('\n'.join(outlines) + '\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-4.5.2/tortoisehg/hgqt/settings.py0000644000175000017500000022316213242076403022076 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os from .qtcore import ( QEvent, QSettings, QTimer, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QCheckBox, QComboBox, QCompleter, QDialog, QDialogButtonBox, QDirModel, QFileDialog, QFont, QFontDialog, QFormLayout, QFrame, QGridLayout, QHBoxLayout, QIcon, QIntValidator, QLabel, QLineEdit, QListView, QListWidget, QListWidgetItem, QMessageBox, QPalette, QPushButton, QRadioButton, QStackedWidget, QStyle, QTabWidget, QTextBrowser, QTextEdit, QVBoxLayout, QWidget, ) from mercurial import ( error, extensions, phases, util, ) from ..util import ( editor, gpg, hglib, i18n, paths, terminal, wconfig, ) from ..util.i18n import _ from . import ( customtools, fileencoding, qscilib, qtlib, thgrepo, ) if os.name == 'nt': from ..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 = qtlib.readStringList(settings, 'settings/' + opts['cpath']) 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() # use QCompleter(model, parent) to avoid ownership bug of # QCompleter(parent /TransferBack/) in PyQt<4.11.4 completer = QCompleter(None, 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, defaults=None): 'EditCombo, only allows integer values' opts['canedit'] = True opts['validator'] = QIntValidator(None) # missing parent=None on PyQt4.6 if defaults: opts['defaults'] = ['%d' % n for n in defaults] 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(hglib.loadui()) def findMergeTools(): return hglib.mergetools(hglib.loadui()) def findEditors(): return editor.findeditors(hglib.loadui()) def findTerminals(): return terminal.findterminals(hglib.loadui()) def findGpg(): return gpg.findgpg(hglib.loadui()) def genCheckBox(opts): opts['nohist'] = True return SettingsCheckBox(**opts) class _fi(object): """Information of each field""" __slots__ = ('label', 'cpath', 'values', 'tooltip', 'restartneeded', 'globalonly', 'noglobal', 'master', 'visible') def __init__(self, label, cpath, values, tooltip, restartneeded=False, globalonly=False, noglobal=False, master=None, visible=None): self.label = label self.cpath = cpath self.values = values self.tooltip = tooltip self.restartneeded = restartneeded self.globalonly = globalonly self.noglobal = noglobal 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'}, ( _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(_('CLI Editor'), 'ui.editor', (genEditableDeferredCombo, findEditors), _('The editor used by Mercurial command line commands to ' 'collect multiline input from the user. Most notably, ' 'commit messages.')), _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. Similarly, %(root)s will be the full path to the ' 'repository. (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'), globalonly=True), _fi(_('Monitor Repo Changes'), 'tortoisehg.monitorrepo', (genDefaultCombo, ['always', 'localonly', 'never']), _('Specify the target filesystem where TortoiseHg monitors changes. ' 'Default: localonly')), _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. This setting is ignored when ' 'TortoiseHg runs as an OS X application bundle. Default: True'), globalonly=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: True.')), _fi(_('New Repo Skeleton'), 'tortoisehg.initskel', genPathBrowser, _('If specified, files in the directory, e.g. .hgignore, are copied ' 'to the newly-created repository.'), globalonly=True), )), ({'name': 'log', 'label': _('Workbench'), 'icon': 'hg-log'}, ( _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', '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'), globalonly=True), _fi(_('Author Coloring'), 'tortoisehg.authorcolor', genBoolRBGroup, _('Color changesets by author name. ' '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 sync grep and pbranch.
' 'Default: log commit grep pbranch | sync'), restartneeded=True, globalonly=True), _fi(_('Long Summary'), 'tortoisehg.longsummary', genBoolRBGroup, _('If true, concatenate multiple lines of changeset summary ' 'and truncate them at 80 characters as necessary. ' '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')), _fi(_('Show Family Line'), 'tortoisehg.showfamilyline', genBoolRBGroup, _('Show indirect revision dependency on the revision graph ' 'when filtered by revset. Default: True

' 'Note: Calculating family line may be slow in some cases. ' 'This option is expected to be removed if the performance issue is ' 'solved.')), _fi(_('Use optimized graph layouter'), 'tortoisehg.graphopt', genBoolRBGroup, _('Use alternative graph layouter for large repositories. ' 'Default: False

' 'Note: This layouter colors edges using branch information ' 'and does not display graft edges, regardless of whether they are ' 'requested or not.')), )), ({'name': 'commit', 'label': _('Commit', 'config item'), 'icon': 'hg-commit'}, ( _fi(_('Username'), 'ui.username', genEditCombo, _('Name associated with commits. The common format is:
' 'Full Name <email@example.com>')), _fi(_('Ask Username'), 'ui.askusername', genBoolRBGroup, _('If no username has been specified, the user will be prompted to ' 'enter a username. Default: False')), _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(_('Check Subrepo Phase'), 'phases.checksubrepos', (genDefaultCombo, ['ignore', 'follow', 'abort']), _('Check the phase of the current revision of each subrepository. ' 'For settings other than "ignore", the phase of the current ' 'revision of each subrepository is checked before committing the ' 'parent repository. ' 'Default: follow')), _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 shown. ' '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 shown. ' '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)')), _fi(_('Subrepository Features:'), None, genSpacer, ''), _fi(_('Allow Hg Subrepos'), 'subrepos.hg:allowed', genBoolRBGroup, _('Whether Mercurial subrepositories are allowed in the working ' 'directory. ' 'Default: True')), _fi(_('Allow Git Subrepos'), 'subrepos.git:allowed', genBoolRBGroup, _('Whether Git subrepositories are allowed in the working ' 'directory. ' 'Default: False' '

See the security note before enabling ' 'Git subrepos.') % 'https://www.mercurial-scm.org/doc/hgrc.5.html#subrepos'), _fi(_('Allow SVN Subrepos'), 'subrepos.svn:allowed', genBoolRBGroup, _('Whether Subversion subrepositories are allowed in the working ' 'directory. ' 'Default: False' '

See the security note before enabling ' 'Subversion subrepos.') % 'https://www.mercurial-scm.org/doc/hgrc.5.html#subrepos'), )), ({'name': 'web', 'label': _('Server'), 'icon': 'hg-serve'}, ( _fi(_('Repository Details:'), 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.'), noglobal=True), _fi(_('Encoding'), 'web.encoding', (genEditCombo, fileencoding.knownencodings()), _('Character encoding of files in the repository, used by the web ' 'interface and TortoiseHg.')), _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(_('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.')), )), ({'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, [25, 465, 587]), _('Port to connect to on mail server. ' 'Default: 25')), _fi(_('SMTP TLS'), 'smtp.tls', (genDefaultCombo, ['starttls', 'smtps', 'none']), _('Method to enable TLS when connecting to mail server. ' 'Default: none')), _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(_('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': 'simplelock', 'label': _('Simplelock'), 'icon': 'thg-password', 'extension': 'simplelock'}, ( _fi(_('Lock Clone'), 'simplelock.repo', genEditCombo, _('Location of local clone of organizational lock repository.

' 'This repository must contain a "locked" text file')), )), ({'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('thg-repoconfig')) 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 https://code.google.com/archive/p/iniparse/' if not focus: focus = qtlib.readString(QSettings(), 'settings/lastpage', 'log') layout = QVBoxLayout() self.setLayout(layout) s = QSettings() self.settings = s self.restoreGeometry(qtlib.readByteArray(s, 'settings/geom')) 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) if qtlib.IS_RETINA: self.conftabs.setIconSize(qtlib.barRetinaIconSize()) utab = SettingsForm(rcpath=hglib.userrcpath(), focus=focus) self.conftabs.addTab(utab, qtlib.geticon('thg-userconfig'), _("%s's global settings") % username()) utab.restartRequested.connect(self._pushRestartRequest) try: if root is None: root = paths.find_root() if root: repo = thgrepo.repository(hglib.loadui(), 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: repoagent = repo._pyqtobj # TODO 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)') % repoagent.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('thg-repoconfig'), _('%s repository settings') % repoagent.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 any(self.conftabs.widget(i).isDirty() for i in xrange(self.conftabs.count())) @pyqtSlot(str) 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 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(str) 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.ClickFocus) p = self.fnedit.palette() p.setColor(QPalette.Base, Qt.transparent) self.fnedit.setPalette(p) 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) if qtlib.IS_RETINA: pageList.setIconSize(qtlib.listviewRetinaIconSize()) 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: icon = self.style().standardIcon(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 == hglib.userrcpath()) elif e.noglobal: w.setEnabled(self.rcpath != hglib.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 hglib.loadui().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-4.5.2/tortoisehg/hgqt/shellconf.py0000644000175000017500000002465113150123225022206 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import from _winreg import ( CreateKey, HKEY_CURRENT_USER, OpenKey, QueryValueEx, REG_DWORD, REG_SZ, SetValueEx, ) from .qtgui import ( QApplication, QCheckBox, QDialog, QDialogButtonBox, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton, QStyle, QTabWidget, QVBoxLayout, QWidget, ) from ..util import menuthg from ..util.i18n import _ from . import qtlib 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() # i18n: URL of TortoiseSVN documentation url = _('https://tortoisesvn.net/docs/release/TortoiseSVN_en/' 'tsvn-dug-settings.html#tsvn-dug-settings-icon-set') w = QLabel(_('You can change the icon set from ' "TortoiseSVN's Settings") % url) w.setOpenExternalLinks(True) iconslayout.addWidget(w) # 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(_("Explorer Extension Settings - TortoiseHg")) self.setWindowIcon(qtlib.geticon('thg-repoconfig')) 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(label['str'].decode('utf-8')) 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-4.5.2/tortoisehg/hgqt/bisect.py0000644000175000017500000001517213150123225021500 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAbstractButton, QCheckBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QLineEdit, QPushButton, QVBoxLayout, ) from mercurial import ( error, util, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, qtlib, ) class BisectDialog(QDialog): newCandidate = pyqtSignal() def __init__(self, repoagent, parent=None): super(BisectDialog, self).__init__(parent) self.setWindowTitle(_('Bisect - %s') % repoagent.displayName()) self.setWindowIcon(qtlib.geticon('hg-bisect')) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() # base layout box box = QVBoxLayout() box.setSpacing(6) self.setLayout(box) form = QFormLayout() box.addLayout(form) hbox = QHBoxLayout() self._gle = gle = QLineEdit() hbox.addWidget(gle, 1) self._gb = gb = QPushButton(_('Accept')) hbox.addWidget(gb) form.addRow(_('Known good revision:'), hbox) hbox = QHBoxLayout() self._ble = ble = QLineEdit() hbox.addWidget(ble, 1) self._bb = bb = QPushButton(_('Accept')) hbox.addWidget(bb) form.addRow(_('Known bad revision:'), hbox) self.discard_chk = QCheckBox(_('Discard local changes ' '(revert --all)')) form.addRow(self.discard_chk) ## command widget self._cmdlog = log = cmdui.LogWidget(self) box.addWidget(log, 1) self._stbar = stbar = cmdui.ThgStatusBar(self) stbar.setSizeGripEnabled(False) box.addWidget(stbar) self._nextbuttons = buttons = QDialogButtonBox(self) buttons.setCenterButtons(True) buttons.clicked.connect(self._markRevision) box.addWidget(buttons) for state, text in [('good', _('Revision is &Good')), ('bad', _('Revision is &Bad')), ('skip', _('&Skip this Revision'))]: btn = buttons.addButton(text, QDialogButtonBox.ActionRole) btn.setObjectName(state) hbox = QHBoxLayout() box.addLayout(hbox) hbox.addStretch() closeb = QPushButton(_('Close')) hbox.addWidget(closeb) closeb.clicked.connect(self.reject) self.goodrev = self.badrev = self.lastrev = None self.restart() gb.clicked.connect(self._verifyGood) bb.clicked.connect(self._verifyBad) gle.returnPressed.connect(self._verifyGood) ble.returnPressed.connect(self._verifyBad) @property def repo(self): return self._repoagent.rawRepo() def restart(self, goodrev=None, badrev=None): if not self._cmdsession.isFinished(): return self._gle.setEnabled(True) self._gle.setText(goodrev or '') self._gb.setEnabled(True) self._ble.setEnabled(False) self._ble.setText(badrev or '') self._bb.setEnabled(False) self._nextbuttons.setEnabled(False) self._cmdlog.clearLog() self._stbar.showMessage('') self.goodrev = self.badrev = self.lastrev = None def _setSession(self, sess): assert self._cmdsession.isFinished() self._cmdsession = sess sess.commandFinished.connect(self._cmdFinished) sess.outputReceived.connect(self._cmdlog.appendLog) sess.progressReceived.connect(self._stbar.setProgress) cmdui.updateStatusMessage(self._stbar, sess) @pyqtSlot(int) def _cmdFinished(self, ret): self._stbar.clearProgress() if ret != 0: self._stbar.showMessage(_('Error encountered.'), True) return self.repo.invalidatedirstate() ctx = self.repo['.'] if ctx.rev() == self.lastrev: self._stbar.showMessage(_('Culprit found.')) return self.lastrev = ctx.rev() self._nextbuttons.setEnabled(True) self._stbar.showMessage('%s: %d (%s) -> %s' % (_('Revision'), ctx.rev(), ctx, _('Test this revision and report findings. ' '(good/bad/skip)'))) self.newCandidate.emit() def _lookupRevision(self, changeid): try: ctx = self.repo[hglib.fromunicode(changeid)] return ctx.rev() except (error.LookupError, error.RepoLookupError), e: self._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._stbar.showMessage(err) @pyqtSlot() def _verifyGood(self): self.goodrev = self._lookupRevision(unicode(self._gle.text()).strip()) if self.goodrev is None: return self._gb.setEnabled(False) self._gle.setEnabled(False) self._bb.setEnabled(True) self._ble.setEnabled(True) self._ble.setFocus() @pyqtSlot() def _verifyBad(self): self.badrev = self._lookupRevision(unicode(self._ble.text()).strip()) if self.badrev is None: return self._ble.setEnabled(False) self._bb.setEnabled(False) cmds = [] if self.discard_chk.isChecked(): cmds.append(hglib.buildcmdargs('revert', all=True)) cmds.append(hglib.buildcmdargs('bisect', reset=True)) cmds.append(hglib.buildcmdargs('bisect', self.goodrev, good=True)) cmds.append(hglib.buildcmdargs('bisect', self.badrev, bad=True)) self._setSession(self._repoagent.runCommandSequence(cmds, self)) @pyqtSlot(QAbstractButton) def _markRevision(self, button): self._nextbuttons.setEnabled(False) state = str(button.objectName()) cmds = [] if self.discard_chk.isChecked(): cmds.append(hglib.buildcmdargs('revert', all=True)) cmds.append(hglib.buildcmdargs('bisect', '.', **{state: True})) self._setSession(self._repoagent.runCommandSequence(cmds, self)) tortoisehg-4.5.2/tortoisehg/hgqt/qfold.py0000644000175000017500000001226513153775104021350 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( QSettings, Qt, pyqtSlot, ) from .qtgui import ( QCheckBox, QDialog, QDialogButtonBox, QGroupBox, QLabel, QListView, QListWidget, QListWidgetItem, QShortcut, QTextEdit, QVBoxLayout, ) from hgext import mq from ..util import hglib from ..util.i18n import _ from . import ( messageentry, qscilib, qtlib, ) class QFoldDialog(QDialog): def __init__(self, repoagent, patches, parent): super(QFoldDialog, self).__init__(parent) self._repoagent = repoagent self.setWindowTitle(_('Patch fold - %s') % repoagent.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 QShortcut('Ctrl+Return', self, self.accept) QShortcut('Ctrl+Enter', self, self.accept) 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(qtlib.readByteArray(s, 'qfold/geom')) def _writesettings(self): s = QSettings() s.setValue('qfold/geom', self.saveGeometry()) tortoisehg-4.5.2/tortoisehg/hgqt/workbench.py0000644000175000017500000013622013242607601022216 0ustar sborhosborho00000000000000# 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 """ from __future__ import absolute_import import os import subprocess import sys from .qtcore import ( QSettings, QT_VERSION, Qt, pyqtSlot, ) from .qtgui import ( QAction, QActionGroup, QApplication, QComboBox, QFileDialog, QKeySequence, QMainWindow, QMenu, QMenuBar, QShortcut, QSizePolicy, QToolBar, qApp, ) from ..util import ( hglib, paths, ) from ..util.i18n import _ from . import ( cmdcore, cmdui, mq, qtlib, repotab, serve, ) from .docklog import LogDockWidget from .reporegistry import RepoRegistryView from .settings import SettingsDialog class Workbench(QMainWindow): """hg repository viewer/browser application""" def __init__(self, ui, repomanager): QMainWindow.__init__(self) self.ui = ui self._repomanager = repomanager self._repomanager.configChanged.connect(self._setupUrlComboIfCurrent) self.setupUi() repomanager.busyChanged.connect(self._onBusyChanged) repomanager.progressReceived.connect(self.statusbar.setRepoProgress) self.reporegistry = rr = RepoRegistryView(repomanager, self) rr.setObjectName('RepoRegistryView') rr.showMessage.connect(self.statusbar.showMessage) rr.openRepo.connect(self.openRepo) rr.removeRepo.connect(self.repoTabsWidget.closeRepo) rr.cloneRepoRequested.connect(self.cloneRepository) rr.progressReceived.connect(self.statusbar.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) cmdagent = cmdcore.CmdAgent(ui, self) self._console = LogDockWidget(repomanager, cmdagent, self) self._console.setObjectName('Log') self._console.hide() self._console.visibilityChanged.connect(self._updateShowConsoleAction) self.addDockWidget(Qt.BottomDockWidgetArea, self._console) self._setupActions() self.restoreSettings() self.repoTabChanged() self.setAcceptDrops(True) self.setIconSize(qtlib.toolBarIconSize()) 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) if QT_VERSION < 0x50000: from .qtgui import ( qt_mac_set_dock_menu, qt_mac_set_menubar_icons, ) qt_mac_set_dock_menu(self.dockMenu) # On Mac OS X, we do not want icons on menus qt_mac_set_menubar_icons(False) else: self.dockMenu.setAsDockMenu() 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.repoTabsWidget = tw = repotab.RepoTabWidget( self.ui, self._repomanager, self) sp = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sp.setHorizontalStretch(1) sp.setVerticalStretch(1) sp.setHeightForWidth(tw.sizePolicy().hasHeightForWidth()) tw.setSizePolicy(sp) tw.currentTabChanged.connect(self.repoTabChanged) tw.currentRepoChanged.connect(self._onCurrentRepoChanged) tw.currentTaskTabChanged.connect(self._updateTaskViewMenu) tw.currentTitleChanged.connect(self._updateWindowTitle) tw.historyChanged.connect(self._updateHistoryActions) tw.makeLogVisible.connect(self._setConsoleVisible) tw.taskTabVisibilityChanged.connect(self._updateTaskTabVisibilityAction) tw.toolbarVisibilityChanged.connect(self._updateToolBarActions) self.setCentralWidget(tw) self.statusbar = cmdui.ThgStatusBar(self) self.setStatusBar(self.statusbar) tw.progressReceived.connect(self.statusbar.setRepoProgress) tw.showMessageSignal.connect(self.statusbar.showMessage) 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') self.menuFile.addActions(self.repoTabsWidget.tabSwitchActions()) newseparator(menu='file') newaction(_('&Settings'), self.editSettings, icon='thg-userconfig', 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) self._actionShowConsole = a = QAction(_('Show Conso&le'), self) a.setCheckable(True) a.setShortcut('Ctrl+L') a.setIcon(qtlib.geticon('thg-console')) a.triggered.connect(self._setConsoleVisible) self.docktbar.addAction(a) self.menuView.addAction(a) self._actionDockedConsole = a = QAction(self) a.setText(_('Place Console in Doc&k Area')) a.setCheckable(True) a.setChecked(True) a.triggered.connect(self._updateDockedConsoleMode) 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, enabled='repoopen', menu='view') self.actionSaveRepos = \ newaction(_("Save Open Repositories on E&xit"), checkable=True, menu='view') self.actionSaveLastSyncPaths = \ newaction(_("Sa&ve Current Sync Paths on Exit"), checkable=True, menu='view') newseparator(menu='view') a = newaction(_('Show Tas&k Tab'), shortcut='Alt+0', checkable=True, enabled='repoopen', menu='view') a.triggered.connect(self._setRepoTaskTabVisible) self.actionTaskTabVisible = a self.actionGroupTaskView = QActionGroup(self) self.actionGroupTaskView.triggered.connect(self._onSwitchRepoTaskTab) def addtaskview(icon, label, name): a = newaction(label, icon=None, checkable=True, data=name, enabled='repoopen', menu='view') a.setIcon(qtlib.geticon(icon)) self.actionGroupTaskView.addAction(a) self.tasktbar.addAction(a) return a # note that 'grep' and 'search' are equivalent taskdefs = { 'commit': ('hg-commit', _('&Commit')), 'pbranch': ('hg-branch', _('&Patch Branch')), 'log': ('hg-log', _("Revision &Details")), 'grep': ('hg-grep', _('&Search')), 'sync': ('thg-sync', _('S&ynchronize')), # 'console' is toggled by "Show Console" action } tasklist = self.ui.configlist('tortoisehg', 'workbench.task-toolbar') if tasklist == []: tasklist = ['log', 'commit', 'grep', 'pbranch', '|', 'sync'] self.actionSelectTaskPbranch = None for taskname in tasklist: taskname = taskname.strip() taskinfo = taskdefs.get(taskname, None) if taskinfo is None: newseparator(toolbar='task') continue tbar = addtaskview(taskinfo[0], taskinfo[1], taskname) if taskname == 'pbranch': self.actionSelectTaskPbranch = tbar newseparator(menu='view') a = newaction(_("&Refresh"), self.refresh, icon='view-refresh', enabled='repoopen', menu='view', toolbar='edit', tooltip=_('Refresh current repository')) a.setShortcuts(QKeySequence.keyBindings(QKeySequence.Refresh) + [QKeySequence('Ctrl+F5')]) # Ctrl+ to ignore status 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')) self.actionAbort = \ newaction(_('Cancel'), self._abortCommands, icon='process-stop', toolbar='edit', tooltip=_('Stop current operation')) self.actionAbort.setEnabled(False) 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') self.actionBack = \ newaction(_("Back"), self._repofwd('back'), icon='go-previous', shortcut=QKeySequence.Back, enabled=False, toolbar='edit') self.actionForward = \ newaction(_("Forward"), self._repofwd('forward'), icon='go-next', shortcut=QKeySequence.Forward, 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) newseparator(toolbar='edit') menuSync = self.menuRepository.addMenu(_('S&ynchronize')) a = newaction(_("&Lock File..."), self._repofwd('lockTool'), icon='thg-password', enabled='repoopen', menu='repository', toolbar='edit', tooltip=_('Lock or unlock files')) self.lockToolAction = a newseparator(menu='repository') newaction(_("&Update..."), self._repofwd('updateToRevision'), icon='hg-update', enabled='repoopen', menu='repository', toolbar='edit', tooltip=_('Update working directory or switch revisions')) newaction(_("&Shelve..."), self._repofwd('shelve'), icon='hg-shelve', enabled='repoopen', menu='repository') newaction(_("&Import Patches..."), self._repofwd('thgimport'), icon='hg-import', enabled='repoopen', menu='repository') newaction(_("U&nbundle..."), self._repofwd('unbundle'), icon='hg-unbundle', enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_('&Merge...'), self._repofwd('mergeWithOtherHead'), icon='hg-merge', enabled='repoopen', menu='repository', toolbar='edit', tooltip=_('Merge with the other head of the current branch')) newaction(_("&Resolve..."), self._repofwd('resolve'), enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("R&ollback/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(_("&Verify"), self._repofwd('verify'), enabled='repoopen', menu='repository') newaction(_("Re&cover"), self._repofwd('recover'), 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(_("&Web Server"), self.serve, menu='repository', icon='hg-serve') 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') 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()) menuSync.addSeparator() action = QAction(_('&Sync Bookmarks...'), self) action.setIcon(qtlib.geticon('thg-sync-bookmarks')) self._actionavails['repoopen'].append(action) action.triggered.connect(self._runSyncBookmarks) menuSync.addAction(action) self._lastRepoSyncPath = {} self.urlCombo = QComboBox(self) self.urlCombo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.urlCombo.currentIndexChanged.connect(self._updateSyncUrl) 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) 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) # Try to select the previously selected path, if any prevpath = self._lastRepoSyncPath.get(hglib.tounicode(repo.root)) if prevpath: idx = self.urlCombo.findText(prevpath) if idx >= 0: self.urlCombo.setCurrentIndex(idx) self.urlCombo.blockSignals(False) self._updateSyncUrlToolTip(self.urlCombo.currentIndex()) @pyqtSlot(str) def _setupUrlComboIfCurrent(self, root): w = self._currentRepoWidget() 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)[opindex] @pyqtSlot(int) def _updateSyncUrl(self, index): self._updateSyncUrlToolTip(index) # save the new url for later recovery reporoot = self.currentRepoRootPath() if not reporoot: return path = self.urlCombo.currentText() self._lastRepoSyncPath[reporoot] = path 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) 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) 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()) 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: if action.shortcut(): tooltip += ' (%s)' % action.shortcut().toString() 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 createPopupMenu(self): """Create new popup menu for toolbars and dock widgets""" menu = super(Workbench, self).createPopupMenu() assert menu # should have toolbar/dock menu # replace default log dock action by customized one menu.insertAction(self._console.toggleViewAction(), self._actionShowConsole) menu.removeAction(self._console.toggleViewAction()) menu.addSeparator() menu.addAction(self._actionDockedConsole) menu.addAction(_('Custom Toolbar &Settings'), self._editCustomToolsSettings) return menu @pyqtSlot(QAction) def _onSwitchRepoTaskTab(self, action): rw = self._currentRepoWidget() if rw: rw.switchToNamedTaskTab(str(action.data())) @pyqtSlot(bool) def _setRepoTaskTabVisible(self, visible): rw = self._currentRepoWidget() if not rw: return rw.setTaskTabVisible(visible) @pyqtSlot(bool) def _setConsoleVisible(self, visible): if self._actionDockedConsole.isChecked(): self._setDockedConsoleVisible(visible) else: self._setConsoleTaskTabVisible(visible) def _setDockedConsoleVisible(self, visible): self._console.setVisible(visible) if visible: # not hook setVisible() or showEvent() in order to move focus # only when console is activated by user action self._console.setFocus() def _setConsoleTaskTabVisible(self, visible): rw = self._currentRepoWidget() if not rw: return if visible: rw.switchToNamedTaskTab('console') else: # it'll be better if it can switch to the last tab rw.switchToPreferredTaskTab() @pyqtSlot() def _updateShowConsoleAction(self): if self._actionDockedConsole.isChecked(): visible = self._console.isVisibleTo(self) enabled = True else: rw = self._currentRepoWidget() visible = bool(rw and rw.currentTaskTabName() == 'console') enabled = bool(rw) self._actionShowConsole.setChecked(visible) self._actionShowConsole.setEnabled(enabled) @pyqtSlot() def _updateDockedConsoleMode(self): docked = self._actionDockedConsole.isChecked() visible = self._actionShowConsole.isChecked() self._console.setVisible(docked and visible) self._setConsoleTaskTabVisible(not docked and visible) self._updateShowConsoleAction() @pyqtSlot(str, bool) def openRepo(self, root, reuse, bundle=None): """Open tab of the specified repo [unicode]""" root = unicode(root) if not root or root.startswith('ssh://'): return if reuse and self.repoTabsWidget.selectRepo(root): return if not self.repoTabsWidget.openRepo(root, bundle): return @pyqtSlot(str) def showRepo(self, root): """Activate the repo tab or open it if not available [unicode]""" self.openRepo(root, True) @pyqtSlot(str, str) def setRevsetFilter(self, path, filter): if self.repoTabsWidget.selectRepo(path): w = self.repoTabsWidget.currentWidget() w.setFilter(filter) def dragEnterEvent(self, event): d = event.mimeData() for u in d.urls(): root = paths.find_root(unicode(u.toLocalFile())) if root: event.setDropAction(Qt.LinkAction) event.accept() break def dropEvent(self, event): accept = False d = event.mimeData() for u in d.urls(): root = paths.find_root(unicode(u.toLocalFile())) if root: self.showRepo(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 = bool(self._currentRepoWidget()) 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._updateTaskTabVisibilityAction() self._updateToolBarActions() @pyqtSlot() def _updateWindowTitle(self): w = self._currentRepoWidget() if not w: 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()) @pyqtSlot() def _updateToolBarActions(self): w = self._currentRepoWidget() if w: self.filtertbaction.setChecked(w.filterBarVisible()) @pyqtSlot() def _updateTaskViewMenu(self): 'Update task tab menu for current repository' repoWidget = self._currentRepoWidget() if not repoWidget: for a in self.actionGroupTaskView.actions(): a.setChecked(False) if self.actionSelectTaskPbranch is not None: self.actionSelectTaskPbranch.setVisible(False) self.lockToolAction.setVisible(False) else: exts = repoWidget.repo.extensions() if self.actionSelectTaskPbranch is not None: self.actionSelectTaskPbranch.setVisible('pbranch' in exts) name = repoWidget.currentTaskTabName() for action in self.actionGroupTaskView.actions(): action.setChecked(str(action.data()) == name) self.lockToolAction.setVisible('simplelock' in exts) self._updateShowConsoleAction() for i, a in enumerate(a for a in self.actionGroupTaskView.actions() if a.isVisible()): a.setShortcut('Alt+%d' % (i + 1)) @pyqtSlot() def _updateTaskTabVisibilityAction(self): rw = self._currentRepoWidget() self.actionTaskTabVisible.setChecked(bool(rw) and rw.isTaskTabVisible()) @pyqtSlot() def _updateHistoryActions(self): 'Update back / forward actions' rw = self._currentRepoWidget() self.actionBack.setEnabled(bool(rw and rw.canGoBack())) self.actionForward.setEnabled(bool(rw and rw.canGoForward())) @pyqtSlot() def repoTabChanged(self): self._updateHistoryActions() self._updateMenu() self._updateWindowTitle() @pyqtSlot(str) def _onCurrentRepoChanged(self, curpath): curpath = unicode(curpath) self._console.setCurrentRepoRoot(curpath or None) self.reporegistry.setActiveTabRepo(curpath) if curpath: repoagent = self._repomanager.repoAgent(curpath) repo = repoagent.rawRepo() self.mqpatches.setRepoAgent(repoagent) self._setupCustomTools(repo.ui) self._setupUrlCombo(repo) self._updateAbortAction(repoagent) else: self.mqpatches.setRepoAgent(None) self.actionAbort.setEnabled(False) @pyqtSlot() def _setHistoryColumns(self): """Display the column selection dialog""" w = self._currentRepoWidget() assert w w.repoview.setHistoryColumns() def _repotogglefwd(self, name): """Return function to forward action to the current repo tab""" def forwarder(checked): w = self._currentRepoWidget() 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._currentRepoWidget() if w: getattr(w, name)(*params, **namedparams) return forwarder @pyqtSlot() def refresh(self): clear = QApplication.keyboardModifiers() & Qt.ControlModifier w = self._currentRepoWidget() if w: # check unnoticed changes to emit corresponding signals repoagent = self._repomanager.repoAgent(w.repoRootPath()) if clear: repoagent.clearStatus() repoagent.pollStatus() # TODO if all objects are responsive to repository signals, some # of the following actions are not necessary w.reload() @pyqtSlot(QAction) def _runSyncAction(self, action): w = self._currentRepoWidget() if w: op = str(action.data()) w.setSyncUrl(self._syncUrlFor(op) or '') getattr(w, op)() @pyqtSlot() def _runSyncBookmarks(self): w = self._currentRepoWidget() if w: # the sync bookmark dialog is bidirectional but is only able to # handle one remote location therefore we use the push location w.setSyncUrl(self._syncUrlFor('push') or '') w.syncBookmark() @pyqtSlot() def _abortCommands(self): root = self.currentRepoRootPath() if not root: return repoagent = self._repomanager.repoAgent(root) repoagent.abortCommands() def _updateAbortAction(self, repoagent): self.actionAbort.setEnabled(repoagent.isBusy()) @pyqtSlot(str) def _onBusyChanged(self, root): repoagent = self._repomanager.repoAgent(root) self._updateAbortAction(repoagent) if not repoagent.isBusy(): self.statusbar.clearRepoProgress(root) self.statusbar.setRepoBusy(root, repoagent.isBusy()) def serve(self): self._dialogs.open(Workbench._createServeDialog) def _createServeDialog(self): w = self._currentRepoWidget() if w: return serve.run(w.repo.ui, root=w.repo.root) else: return serve.run(self.ui) def loadall(self): w = self._currentRepoWidget() if w: w.repoview.model().loadall() def _gotorev(self): rev, ok = qtlib.getTextInput(self, _("Goto revision"), _("Enter revision identifier")) if ok: w = self._currentRepoWidget() assert w w.gotoRev(rev) @pyqtSlot(str) def gotorev(self, rev): w = self._currentRepoWidget() if w: w.repoview.goto(rev) def newWorkbench(self): cmdline = list(paths.get_thg_command()) cmdline.extend(['workbench', '--nofork', '--newworkbench']) subprocess.Popen(cmdline, creationflags=qtlib.openflags) def newRepository(self): """ Run init dialog """ from tortoisehg.hgqt.hginit import InitDialog path = self.currentRepoRootPath() or '.' dlg = InitDialog(self.ui, path, self) if dlg.exec_() == 0: self.openRepo(dlg.destination(), False) @pyqtSlot() @pyqtSlot(str) def cloneRepository(self, uroot=None): """ Run clone dialog """ # it might be better to reuse existing CloneDialog dlg = self._dialogs.openNew(Workbench._createCloneDialog) if not uroot: uroot = self.currentRepoRootPath() if uroot: dlg.setSource(uroot) dlg.setDestination(uroot + '-clone') def _createCloneDialog(self): from tortoisehg.hgqt.clone import CloneDialog dlg = CloneDialog(self.ui, parent=self) dlg.clonedRepository.connect(self._openClonedRepo) return dlg @pyqtSlot(str, str) def _openClonedRepo(self, root, sourceroot): root = unicode(root) sourceroot = unicode(sourceroot) self.reporegistry.addClonedRepo(root, sourceroot) self.showRepo(root) def openRepository(self): """ Open repo from File menu """ caption = _('Select repository directory to open') root = self.currentRepoRootPath() if root: cwd = os.path.dirname(root) else: cwd = os.getcwdu() FD = QFileDialog path = FD.getExistingDirectory(self, caption, cwd, FD.ShowDirsOnly | FD.ReadOnly) self.openRepo(path, False) def _currentRepoWidget(self): return self.repoTabsWidget.currentWidget() def currentRepoRootPath(self): return self.repoTabsWidget.currentRepoRootPath() 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._currentRepoWidget() 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, repostosave, lastactiverepo): s = QSettings() wb = "Workbench/" s.setValue(wb + 'geometry', self.saveGeometry()) s.setValue(wb + 'windowState', self.saveState()) s.setValue(wb + 'dockedConsole', self._actionDockedConsole.isChecked()) s.setValue(wb + 'saveRepos', self.actionSaveRepos.isChecked()) s.setValue(wb + 'saveLastSyncPaths', self.actionSaveLastSyncPaths.isChecked()) s.setValue(wb + 'lastactiverepo', lastactiverepo) s.setValue(wb + 'openrepos', (',').join(repostosave)) s.beginWriteArray('lastreposyncpaths') lastreposyncpaths = {} if self.actionSaveLastSyncPaths.isChecked(): lastreposyncpaths = self._lastRepoSyncPath for n, root in enumerate(sorted(lastreposyncpaths)): s.setArrayIndex(n) s.setValue('root', root) s.setValue('path', self._lastRepoSyncPath[root]) s.endArray() def restoreSettings(self): s = QSettings() wb = "Workbench/" self.restoreGeometry(qtlib.readByteArray(s, wb + 'geometry')) self.restoreState(qtlib.readByteArray(s, wb + 'windowState')) self._actionDockedConsole.setChecked( qtlib.readBool(s, wb + 'dockedConsole', True)) lastreposyncpaths = {} npaths = s.beginReadArray('lastreposyncpaths') for n in range(npaths): s.setArrayIndex(n) root = qtlib.readString(s, 'root') lastreposyncpaths[root] = qtlib.readString(s, 'path') s.endArray() self._lastRepoSyncPath = lastreposyncpaths save = qtlib.readBool(s, wb + 'saveRepos') self.actionSaveRepos.setChecked(save) savelastsyncpaths = qtlib.readBool(s, wb + 'saveLastSyncPaths') self.actionSaveLastSyncPaths.setChecked(savelastsyncpaths) openreposvalue = qtlib.readString(s, wb + 'openrepos') if openreposvalue: openrepos = openreposvalue.split(',') else: openrepos = [] # Note that if a "root" has been passed to the "thg" command, # "lastactiverepo" will have no effect lastactiverepo = qtlib.readString(s, wb + 'lastactiverepo') self.repoTabsWidget.restoreRepos(openrepos, lastactiverepo) # 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): if self.repoTabsWidget.selectRepo(hglib.tounicode(root)): rw = self.repoTabsWidget.currentWidget() rw.goto(rev) def closeEvent(self, event): repostosave = [] lastactiverepo = '' if self.actionSaveRepos.isChecked(): tw = self.repoTabsWidget repostosave = map(tw.repoRootPath, xrange(tw.count())) lastactiverepo = tw.currentRepoRootPath() if not self.repoTabsWidget.closeAllTabs(): event.ignore() else: self._storeSettings(repostosave, lastactiverepo) self.reporegistry.close() @pyqtSlot() def closeCurrentRepoTab(self): """close the current repo tab""" self.repoTabsWidget.closeTab(self.repoTabsWidget.currentIndex()) def explore(self): root = self.currentRepoRootPath() if root: qtlib.openlocalurl(hglib.fromunicode(root)) def terminal(self): w = self._currentRepoWidget() if w: qtlib.openshell(w.repo.root, hglib.fromunicode(w.repoDisplayName()), w.repo.ui) @pyqtSlot() def editSettings(self, focus=None): sd = SettingsDialog(configrepo=False, focus=focus, parent=self, root=hglib.fromunicode(self.currentRepoRootPath())) sd.exec_() @pyqtSlot() def _editCustomToolsSettings(self): self.editSettings('tools') tortoisehg-4.5.2/tortoisehg/hgqt/quickop.py0000644000175000017500000002606213153775104021716 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import sys from .qtcore import ( QObject, QSettings, Qt, ) from .qtgui import ( QCheckBox, QDialog, QDialogButtonBox, QKeySequence, QLabel, QHBoxLayout, QPushButton, QShortcut, QVBoxLayout, ) from mercurial import util from ..util import ( hglib, shlib, ) from ..util.i18n import _ from . import ( cmdcore, cmdui, lfprompt, qtlib, status, ) 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': 'hg-add', '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) # Handle rm alias if command == 'rm': command = 'remove' self.command = command self.setWindowTitle(_('%s - hg %s') % (repoagent.displayName(), command)) self.setWindowIcon(qtlib.geticon(ICONS[command])) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 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(qtlib.readByteArray(s, 'quickop/geom')) if hasattr(self, 'chk'): if self.command == 'revert': self.chk.setChecked( qtlib.readBool(s, 'quickop/nobackup', True)) elif self.command == 'remove': self.chk.setChecked( qtlib.readBool(s, 'quickop/forceremove', False)) 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.setProgress) self._cmddialog.setSession(sess) self.bb.button(QDialogButtonBox.Ok).setEnabled(False) def commandFinished(self, ret): self.bb.button(QDialogButtonBox.Ok).setEnabled(True) self.statusbar.clearProgress() 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 if self.command == 'remove': self.repo.lfstatus = True try: repostate = self.repo.status() except (EnvironmentError, util.Abort), e: qtlib.WarningMsgBox(_('Unable to read repository status'), hglib.tounicode(str(e)), parent=self) return finally: self.repo.lfstatus = False 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: os.chdir(repo.root) # for scmutil.match() in StatusThread return QuickOpDialog(repoagent, command, pats, None) tortoisehg-4.5.2/tortoisehg/hgqt/matching.py0000644000175000017500000002550013153775104022031 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( QSettings, Qt, pyqtSlot, ) from .qtgui import ( QAbstractButton, QButtonGroup, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QGridLayout, QLabel, QLayout, QSizePolicy, QVBoxLayout, ) from mercurial import error from ..util import hglib from ..util.i18n import _ from . import ( csinfo, qtlib, ) class MatchDialog(QDialog): def __init__(self, repoagent, rev=None, parent=None): super(MatchDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & \ ~Qt.WindowContextHelpButtonHint) self.revsetexpression = '' self._repoagent = repoagent # base layout box box = QVBoxLayout() box.setSpacing(6) ## main layout grid self.grid = QGridLayout() self.grid.setSpacing(6) self.grid.setColumnStretch(1, 1) box.addLayout(self.grid) ### matched revision combo self.rev_combo = combo = QComboBox() combo.setEditable(True) combo.setMinimumContentsLength(30) # cut long name self.grid.addWidget(QLabel(_('Find revisions matching fields of:')), 0, 0) self.grid.addWidget(combo, 0, 1) 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()) + self.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.setVisible(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.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) 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( qtlib.readBool(s, 'matching/summary', False)) self.description_chk.setChecked( qtlib.readBool(s, 'matching/description', True)) self.author_chk.setChecked(qtlib.readBool(s, 'matching/author', True)) self.branch_chk.setChecked(qtlib.readBool(s, 'matching/branch', False)) self.date_chk.setChecked(qtlib.readBool(s, 'matching/date', True)) self.files_chk.setChecked(qtlib.readBool(s, 'matching/files', False)) self.diff_chk.setChecked(qtlib.readBool(s, 'matching/diff', False)) self.parents_chk.setChecked( qtlib.readBool(s, 'matching/parents', False)) self.phase_chk.setChecked(qtlib.readBool(s, 'matching/phase', False)) self.substate_chk.setChecked( qtlib.readBool(s, 'matching/substate', False)) ## 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.SetMinAndMaxSize) self.setWindowTitle(_('Find matches - %s') % repoagent.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 ### @property def repo(self): return self._repoagent.rawRepo() 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()) @pyqtSlot() def update_info(self): 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.setVisible(False) self.rev_to_match_info.setVisible(True) else: self.rev_to_match_info.setVisible(False) self.rev_to_match_info_text.setVisible(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: rset = self.repo.revs(new_rev) 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.first()) validrevset = True except (error.LookupError, error.RepoLookupError): csinfo_set_text(_('Unknown revision!')) except error.ParseError: csinfo_set_text(_('Parse Error!')) self.match_btn.setEnabled(validrevset) 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.setVisible(visible) @pyqtSlot(QAbstractButton) def _selectSummaryOrDescription(self, btn): # Uncheck all other buttons for b in self.desc_btngroup.buttons(): if b is not btn: b.setChecked(False) tortoisehg-4.5.2/tortoisehg/hgqt/webconf.ui0000644000175000017500000000614013150123225021632 0ustar sborhosborho00000000000000 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-4.5.2/tortoisehg/hgqt/repotab.py0000644000175000017500000003674113150123225021670 0ustar sborhosborho00000000000000# repotab.py - stack of repository widgets # # Copyright (C) 2007-2010 Logilab. All rights reserved. # Copyright 2014 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. from __future__ import absolute_import import os from .qtcore import ( QPoint, QSignalMapper, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAction, QActionGroup, QMenu, QStackedLayout, QTabBar, QVBoxLayout, QWidget, ) from mercurial import error from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, qtlib, repowidget, ) class _TabBar(QTabBar): def mouseReleaseEvent(self, event): if event.button() == Qt.MidButton: self.tabCloseRequested.emit(self.tabAt(event.pos())) super(_TabBar, self).mouseReleaseEvent(event) class RepoTabWidget(QWidget): """Manage stack of RepoWidgets of open repositories""" currentRepoChanged = pyqtSignal(str, str) # curpath, prevpath currentTabChanged = pyqtSignal(int) currentTaskTabChanged = pyqtSignal() currentTitleChanged = pyqtSignal() historyChanged = pyqtSignal() makeLogVisible = pyqtSignal(bool) progressReceived = pyqtSignal(str, cmdcore.ProgressMessage) showMessageSignal = pyqtSignal(str) taskTabVisibilityChanged = pyqtSignal(bool) toolbarVisibilityChanged = pyqtSignal(bool) # look-up of tab-index and stack-index: # 1. tabbar[tab-index] -> {tabData: rw, tabToolTip: root} # 2. stack[rw] -> stack-index # # tab-index is the master, so do not use stack.setCurrentIndex(). def __init__(self, ui, repomanager, parent=None): super(RepoTabWidget, self).__init__(parent) self._ui = ui self._repomanager = repomanager # delay until the next event loop so that the current tab won't be # gone in the middle of switching tabs (issue #4253) repomanager.repositoryDestroyed.connect(self.closeRepo, Qt.QueuedConnection) vbox = QVBoxLayout(self) vbox.setContentsMargins(0, 0, 0, 0) vbox.setSpacing(0) self._tabbar = tabbar = _TabBar(self) if qtlib.IS_RETINA: tabbar.setIconSize(qtlib.barRetinaIconSize()) tabbar.setDocumentMode(True) tabbar.setExpanding(False) tabbar.setTabsClosable(True) tabbar.setMovable(True) tabbar.currentChanged.connect(self._onCurrentTabChanged) tabbar.tabCloseRequested.connect(self.closeTab) tabbar.hide() vbox.addWidget(tabbar) self._initTabMenuActions() tabbar.setContextMenuPolicy(Qt.CustomContextMenu) tabbar.customContextMenuRequested.connect(self._onTabMenuRequested) self._initTabSwitchActions() tabbar.tabMoved.connect(self._updateTabSwitchActions) self._stack = QStackedLayout() vbox.addLayout(self._stack, 1) self._curpath = '' # != currentRepoRootPath until _onCurrentTabChanged self._lastclickedindex = -1 self._lastclosedpaths = [] self._iconmapper = QSignalMapper(self) self._iconmapper.mapped[QWidget].connect(self._updateIcon) self._titlemapper = QSignalMapper(self) self._titlemapper.mapped[QWidget].connect(self._updateTitle) self._updateTabSwitchActions() def openRepo(self, root, bundle=None): """Open the specified repository in new tab""" rw = self._createRepoWidget(root, bundle) if not rw: return False # do not emit currentChanged until tab properties are fully set up. # the first tab is automatically selected. tabbar = self._tabbar tabbar.blockSignals(True) index = tabbar.insertTab(self._newTabIndex(), rw.title()) tabbar.setTabData(index, rw) tabbar.setTabToolTip(index, rw.repoRootPath()) self.setCurrentIndex(index) tabbar.blockSignals(False) self._updateTabSwitchActions() self._updateTabVisibility() self._onCurrentTabChanged(index) return True def _addUnloadedRepos(self, rootpaths): """Add tabs of the specified repositories without loading them""" tabbar = self._tabbar tabbar.blockSignals(True) for index, root in enumerate(rootpaths, self._newTabIndex()): root = hglib.normreporoot(root) index = tabbar.insertTab(index, os.path.basename(root)) tabbar.setTabToolTip(index, root) tabbar.blockSignals(False) self._updateTabSwitchActions() self._updateTabVisibility() # must call _onCurrentTabChanged() appropriately def _newTabIndex(self): if self._ui.configbool('tortoisehg', 'opentabsaftercurrent', True): return self.currentIndex() + 1 else: return self.count() @pyqtSlot(str) def closeRepo(self, root): """Close tabs of the specified repository""" root = hglib.normreporoot(root) return self._closeTabs(list(self._findIndexesByRepoRootPath(root))) @pyqtSlot(int) def closeTab(self, index): if 0 <= index < self.count(): return self._closeTabs([index]) return False def closeAllTabs(self): return self._closeTabs(range(self.count())) def _closeTabs(self, indexes): if not self._checkTabsClosable(indexes): return False self._lastclosedpaths = map(self.repoRootPath, indexes) self._removeTabs(indexes) return True def _checkTabsClosable(self, indexes): for i in indexes: rw = self._widget(i) if rw and not rw.closeRepoWidget(): self.setCurrentIndex(i) return False return True def _removeTabs(self, indexes): # must call _checkRepoTabsClosable() before indexes = sorted(indexes, reverse=True) tabchange = indexes and indexes[-1] <= self.currentIndex() self._tabbar.blockSignals(True) for i in indexes: rw = self._widget(i) self._tabbar.removeTab(i) if rw: self._stack.removeWidget(rw) self._repomanager.releaseRepoAgent(rw.repoRootPath()) rw.deleteLater() self._tabbar.blockSignals(False) self._updateTabSwitchActions() self._updateTabVisibility() if tabchange: self._onCurrentTabChanged(self.currentIndex()) def selectRepo(self, root): """Find the tab for the specified repository and make it current""" root = hglib.normreporoot(root) if self.currentRepoRootPath() == root: return True for i in self._findIndexesByRepoRootPath(root): self.setCurrentIndex(i) return True return False def restoreRepos(self, rootpaths, activepath): """Restore tabs of the last open repositories""" if not rootpaths: return self._addUnloadedRepos(rootpaths) self._tabbar.blockSignals(True) self.selectRepo(activepath) self._tabbar.blockSignals(False) self._onCurrentTabChanged(self.currentIndex()) def _initTabMenuActions(self): actiondefs = [ ('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), ] self._actions = {} for name, desc, tip, cb in actiondefs: self._actions[name] = act = QAction(desc, self) act.setStatusTip(tip) act.triggered.connect(cb) self.addAction(act) @pyqtSlot(QPoint) def _onTabMenuRequested(self, point): index = self._tabbar.tabAt(point) if index >= 0: self._lastclickedindex = index else: self._lastclickedindex = self.currentIndex() menu = QMenu(self) menu.addAction(self._actions['closetab']) menu.addAction(self._actions['closeothertabs']) menu.addSeparator() if len(self._lastclosedpaths) > 1: menu.addAction(self._actions['reopenlastclosedgroup']) elif self._lastclosedpaths: menu.addAction(self._actions['reopenlastclosed']) menu.setAttribute(Qt.WA_DeleteOnClose) menu.popup(self._tabbar.mapToGlobal(point)) @pyqtSlot() def _closeLastClickedTab(self): self.closeTab(self._lastclickedindex) @pyqtSlot() def _closeNotLastClickedTabs(self): if self._lastclickedindex >= 0: self._closeTabs([i for i in xrange(self.count()) if i != self._lastclickedindex]) @pyqtSlot() def _reopenLastClosedTabs(self): origindex = self.currentIndex() self._addUnloadedRepos(self._lastclosedpaths) del self._lastclosedpaths[:] if origindex != self.currentIndex(): self._onCurrentTabChanged(self.currentIndex()) def tabSwitchActions(self): """List of actions to switch current tabs; should be registered to the main window or menu""" return self._swactions.actions() def _initTabSwitchActions(self): self._swactions = QActionGroup(self) self._swactions.triggered.connect(self._setCurrentTabByAction) for i in xrange(9): a = self._swactions.addAction('') a.setCheckable(True) a.setData(i) a.setShortcut('Ctrl+%d' % (i + 1)) @pyqtSlot() def _updateTabSwitchActions(self): self._swactions.setVisible(self._tabbar.count() > 1) if not self._swactions.isVisible(): return for i, a in enumerate(self._swactions.actions()): a.setVisible(i < self.count()) if not a.isVisible(): continue a.setChecked(i == self.currentIndex()) a.setText(self._tabbar.tabText(i)) @pyqtSlot(QAction) def _setCurrentTabByAction(self, action): index = action.data() self.setCurrentIndex(index) def currentRepoRootPath(self): return self.repoRootPath(self.currentIndex()) def repoRootPath(self, index): return unicode(self._tabbar.tabToolTip(index)) def _findIndexesByRepoRootPath(self, root): for i in xrange(self.count()): if self.repoRootPath(i) == root: yield i def count(self): """Number of tabs including repositories of not-yet opened""" return self._tabbar.count() def currentIndex(self): return self._tabbar.currentIndex() def currentWidget(self): return self._stack.currentWidget() @pyqtSlot(int) def setCurrentIndex(self, index): self._tabbar.setCurrentIndex(index) @pyqtSlot(int) def _onCurrentTabChanged(self, index): rw = self._widget(index) if not rw and index >= 0: tabbar = self._tabbar rw = self._createRepoWidget(self.repoRootPath(index)) if not rw: tabbar.removeTab(index) # may reenter self._updateTabSwitchActions() self._updateTabVisibility() return tabbar.setTabData(index, rw) tabbar.setTabText(index, rw.title()) # update path in case filesystem changed after tab was added tabbar.setTabToolTip(index, rw.repoRootPath()) if rw: self._stack.setCurrentWidget(rw) self._updateTabSwitchActions() prevpath = self._curpath self._curpath = self.repoRootPath(index) self.currentTabChanged.emit(index) # there may be more than one tabs of the same repo if self._curpath != prevpath: self._onCurrentRepoChanged(self._curpath, prevpath) def _onCurrentRepoChanged(self, curpath, prevpath): prevrepoagent = currepoagent = None if prevpath: prevrepoagent = self._repomanager.repoAgent(prevpath) # may be None if curpath: currepoagent = self._repomanager.repoAgent(curpath) if prevrepoagent: prevrepoagent.suspendMonitoring() if currepoagent: currepoagent.resumeMonitoring() self.currentRepoChanged.emit(curpath, prevpath) def _indexOf(self, rw): if self.currentWidget() is rw: return self.currentIndex() # fast path for i in xrange(self.count()): if self._widget(i) is rw: return i return -1 def _widget(self, index): return self._tabbar.tabData(index) def _createRepoWidget(self, root, bundle=None): try: repoagent = self._repomanager.openRepoAgent(root) except (error.Abort, error.RepoError), e: qtlib.WarningMsgBox(_('Failed to open repository'), hglib.tounicode(str(e)), parent=self) return rw = repowidget.RepoWidget(repoagent, self, bundle=bundle) rw.currentTaskTabChanged.connect(self.currentTaskTabChanged) rw.makeLogVisible.connect(self.makeLogVisible) rw.progress.connect(self._mapProgressReceived) rw.repoLinkClicked.connect(self._openLinkedRepo) rw.revisionSelected.connect(self.historyChanged) rw.showMessageSignal.connect(self.showMessageSignal) rw.taskTabVisibilityChanged.connect(self.taskTabVisibilityChanged) rw.toolbarVisibilityChanged.connect(self.toolbarVisibilityChanged) rw.busyIconChanged.connect(self._iconmapper.map) self._iconmapper.setMapping(rw, rw) rw.titleChanged.connect(self._titlemapper.map) self._titlemapper.setMapping(rw, rw) self._stack.addWidget(rw) return rw @pyqtSlot(str, object, str, str, object) def _mapProgressReceived(self, topic, pos, item, unit, total): rw = self.sender() assert isinstance(rw, repowidget.RepoWidget) progress = cmdcore.ProgressMessage( unicode(topic), pos, unicode(item), unicode(unit), total) self.progressReceived.emit(rw.repoRootPath(), progress) @pyqtSlot(str) def _openLinkedRepo(self, path): uri = unicode(path).split('?', 1) path = hglib.normreporoot(uri[0]) rev = None if len(uri) > 1: rev = hglib.fromunicode(uri[1]) if self.selectRepo(path) or self.openRepo(path): rw = self.currentWidget() 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.switchToNamedTaskTab('commit') @pyqtSlot('QWidget*') def _updateIcon(self, rw): index = self._indexOf(rw) self._tabbar.setTabIcon(index, rw.busyIcon()) @pyqtSlot('QWidget*') def _updateTitle(self, rw): index = self._indexOf(rw) self._tabbar.setTabText(index, rw.title()) self._updateTabSwitchActions() if index == self.currentIndex(): self.currentTitleChanged.emit() def _updateTabVisibility(self): forcetab = self._ui.configbool('tortoisehg', 'forcerepotab') self._tabbar.setVisible(self.count() > 1 or (self.count() == 1 and forcetab)) tortoisehg-4.5.2/tortoisehg/hgqt/docklog.py0000644000175000017500000005005713150123225021652 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os from .qsci import ( QsciScintilla, ) from .qtcore import ( QIODevice, QProcess, QTimer, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QColor, QDockWidget, QStackedWidget, QVBoxLayout, QWidget, ) from mercurial import ( commands, util, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdui, qtlib, ) class _LogWidgetForConsole(cmdui.LogWidget): """Wrapped LogWidget for ConsoleWidget""" returnPressed = pyqtSignal(str) """Return key pressed when cursor is on prompt line""" historyRequested = pyqtSignal(str, int) # keyword, direction completeRequested = pyqtSignal(str) _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, qtlib.TaskWidget): """Console to run hg/thg command and show output""" closeRequested = pyqtSignal() def __init__(self, agent, parent=None): QWidget.__init__(self, 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.appendLog) if util.safehasattr(agent, 'displayName'): self._logwidget.setPrompt('%s%% ' % agent.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(str, 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(str) 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.warning') @pyqtSlot(str, str) def appendLog(self, msg, label): """Append log text to the last line while keeping the prompt line""" 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(str) def _runcommand(self, cmdline): cmdline = unicode(cmdline) self._commandIdx = 0 try: args = hglib.parsecmdline(cmdline, self._workingDirectory()) 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 _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, self) 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): def __init__(self, repomanager, cmdagent, parent=None): super(LogDockWidget, self).__init__(parent) self.setFeatures(QDockWidget.DockWidgetClosable | QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) self.setWindowTitle(_('Console')) # 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(cmdagent) for root in self._repomanager.repoRootPaths(): self._createConsoleFor(root) def setCurrentRepoRoot(self, root): w = self._findConsoleFor(root) self._consoles.setCurrentWidget(w) self.setFocusProxy(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) self._consoles.addWidget(w) return w @pyqtSlot(str) def _createConsoleFor(self, root): root = unicode(root) repoagent = self._repomanager.repoAgent(root) assert repoagent self._createConsole(repoagent) @pyqtSlot(str) def _destroyConsoleFor(self, root): root = unicode(root) w = self._findConsoleFor(root) self._consoles.removeWidget(w) w.setParent(None) 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-4.5.2/tortoisehg/hgqt/revert.py0000644000175000017500000001013213150123225021525 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( Qt, pyqtSlot, ) from .qtgui import ( QCheckBox, QComboBox, QDialog, QDialogButtonBox, QLabel, QVBoxLayout, ) from mercurial.node import nullid from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, qtlib, ) class RevertDialog(QDialog): def __init__(self, repoagent, wfiles, rev, parent): super(RevertDialog, self).__init__(parent) self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() self.setWindowTitle(_('Revert - %s') % repoagent.displayName()) f = self.windowFlags() self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint) repo = repoagent.rawRepo() 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()) 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-4.5.2/tortoisehg/hgqt/qtcore.py0000644000175000017500000000264613155510714021536 0ustar sborhosborho00000000000000# qtcore.py - PyQt4/5 compatibility wrapper # # Copyright 2015 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. """Thin compatibility wrapper for QtCore""" from __future__ import absolute_import import os import sys def _detectapi(): candidates = ['PyQt5', 'PyQt4'] if not getattr(sys, 'frozen', False): api = os.environ.get('THG_QT_API') if api: return api for api in candidates: try: mod = __import__(api) mod.__name__ # get around demandimport return api except ImportError: pass return candidates[0] try: from ..util.config import qt_api as QT_API except (AttributeError, ImportError): QT_API = _detectapi() if QT_API == 'PyQt4': def _fixapi(): import sip for e in ['QDate', 'QDateTime', 'QString', 'QTextStream', 'QTime', 'QUrl', 'QVariant']: sip.setapi(e, 2) _fixapi() from PyQt4.QtCore import * from PyQt4.QtGui import ( QAbstractProxyModel, QItemSelection, QItemSelectionModel, QItemSelectionRange, QSortFilterProxyModel, QStringListModel, ) del SIGNAL, SLOT elif QT_API == 'PyQt5': from PyQt5.QtCore import * else: raise RuntimeError('unsupported Qt API: %s' % QT_API) tortoisehg-4.5.2/tortoisehg/hgqt/qrename.py0000644000175000017500000000723613150123225021661 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os from .qtcore import ( Qt, pyqtSlot, ) from .qtgui import ( QDialog, QDialogButtonBox, QLabel, QRadioButton, QVBoxLayout, ) from ..util import hglib from ..util.i18n import _ from . 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-4.5.2/tortoisehg/hgqt/chunks.py0000644000175000017500000007200013242076403021522 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import cStringIO import os import re from . import qsci as Qsci from .qtcore import ( QPoint, QTimer, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAction, QColor, QDialog, QFontMetrics, QFrame, QHBoxLayout, QKeySequence, QLabel, QMenu, QPainter, QSplitter, QStyle, QToolBar, QToolButton, QVBoxLayout, QWidget, ) from mercurial import ( commands, match as matchmod, patch, util, ) from ..util import hglib from ..util.patchctx import patchctx from ..util.i18n import _ from . import ( blockmatcher, filedata, filelistview, lexers, manifestmodel, qscilib, qtlib, rejects, revert, visdiff, ) # TODO # Add support for tools like TortoiseMerge that help resolve rejected chunks qsci = Qsci.QsciScintilla class ChunksWidget(QWidget): linkActivated = pyqtSignal(str) showMessage = pyqtSignal(str) chunksSelected = pyqtSignal(bool) fileSelected = pyqtSignal(bool) fileModelEmpty = pyqtSignal(bool) fileModified = pyqtSignal() contextmenu = None def __init__(self, repoagent, parent): QWidget.__init__(self, parent) self._repoagent = repoagent self.currentFile = None layout = QVBoxLayout(self) layout.setSpacing(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(self) model = manifestmodel.ManifestModel( repoagent, self, statusfilter='MAR', flat=True) self.filelist.setModel(model) 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.setContentsMargins(0, 0, 0, 0) vbox.addWidget(self.filelist) self.fileListFrame.setLayout(vbox) self.diffbrowse = DiffBrowser(self.splitter) 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): # don't repo.ui.copy(), which is protected to clone baseui since hg 2.9 ui = self.repo.ui class warncapt(ui.__class__): def warn(self, msg, *args, **opts): self.write(msg) ui = warncapt(ui) 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.wvfs(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 = patch.parsepatch(buf) if chunks: header = chunks[0] return [header] + header.hunks else: return [] @pyqtSlot(str, str) def displayFile(self, file, status): if isinstance(file, unicode): 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.model().setRawContext(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(str) showMessage = pyqtSignal(str) 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() self.searchbar.hide() self.searchbar.searchRequested.connect(self.find) self.searchbar.conditionChanged.connect(self.highlightText) self.addActions(self.searchbar.editorActions()) self.sumlabel = QLabel() self.allbutton = QToolButton() self.allbutton.setText(_('All', 'files')) self.allbutton.setShortcut(QKeySequence.SelectAll) self.allbutton.clicked.connect(self.selectAll) self.nonebutton = QToolButton() 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(qtlib.smallIconSize()) 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.setFont(qtlib.getfont('fontdiff').font()) 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.createFileData(self._ctx, None, filename, status) fd.load(force=force) fd.detectTextEncoding() 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 = patch.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.append(buf.getvalue().decode(fd.textEncoding(), 'replace')) 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(str, bool, bool, bool) def find(self, exp, icase=True, wrap=False, forward=True): self.sci.find(exp, icase, wrap, forward) @pyqtSlot(str, 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-4.5.2/tortoisehg/hgqt/prune.py0000644000175000017500000000772313173404245021374 0ustar sborhosborho00000000000000# prune.py - simple dialog to prune revisions # # Copyright 2014 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. from __future__ import absolute_import from .qtcore import ( QTimer, pyqtSlot, ) from .qtgui import ( QCheckBox, QComboBox, QFormLayout, QSizePolicy, QVBoxLayout, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, cslist, qtlib, ) class PruneWidget(cmdui.AbstractCmdWidget): def __init__(self, repoagent, parent=None): super(PruneWidget, self).__init__(parent) self._repoagent = repoagent self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) vbox = QVBoxLayout(self) form = QFormLayout() vbox.addLayout(form) self._revedit = w = QComboBox(self) w.setEditable(True) qtlib.allowCaseChangingInput(w) w.installEventFilter(qtlib.BadCompletionBlocker(w)) w.activated.connect(self._updateRevset) w.lineEdit().textEdited.connect(self._onRevsetEdited) form.addRow(_('Target:'), w) optbox = QVBoxLayout() form.addRow('', optbox) self._optchks = {} for name, text in [ ('keep', _('Do not modify working copy (-k/--keep)')), ]: self._optchks[name] = w = QCheckBox(text, self) optbox.addWidget(w) repo = repoagent.rawRepo() self._cslist = w = cslist.ChangesetList(repo, self) vbox.addWidget(w) self._querysess = cmdcore.nullCmdSession() # slightly longer delay than common keyboard auto-repeat rate self._querylater = QTimer(self, interval=550, singleShot=True) self._querylater.timeout.connect(self._updateRevset) self._revedit.setFocus() def revset(self): return unicode(self._revedit.currentText()) def setRevset(self, revspec): if self.revset() == unicode(revspec): return w = self._revedit i = w.findText(revspec) if i < 0: i = 0 w.insertItem(i, revspec) w.setCurrentIndex(i) self._updateRevset() @pyqtSlot() def _onRevsetEdited(self): self._querysess.abort() self._querylater.start() self.commandChanged.emit() @pyqtSlot() def _updateRevset(self): self._querysess.abort() self._querylater.stop() cmdline = hglib.buildcmdargs('log', rev=self.revset(), T='{rev}\n') self._querysess = sess = self._repoagent.runCommand(cmdline, self) sess.setCaptureOutput(True) sess.commandFinished.connect(self._onQueryFinished) self.commandChanged.emit() @pyqtSlot(int) def _onQueryFinished(self, ret): sess = self._querysess if not sess.isFinished() or self._querylater.isActive(): # new query is already or about to be running return if ret == 0: revs = map(int, str(sess.readAll()).splitlines()) else: revs = [] self._cslist.update(revs) self.commandChanged.emit() def canRunCommand(self): sess = self._querysess return (sess.isFinished() and sess.exitCode() == 0 and not self._querylater.isActive()) def runCommand(self): opts = {} opts.update((n, w.isChecked()) for n, w in self._optchks.iteritems()) cmdline = hglib.buildcmdargs('prune', rev=self.revset(), **opts) return self._repoagent.runCommand(cmdline, self) def createPruneDialog(repoagent, revspec, parent=None): dlg = cmdui.CmdControlDialog(parent) dlg.setWindowIcon(qtlib.geticon('edit-cut')) dlg.setWindowTitle(_('Prune - %s') % repoagent.displayName()) dlg.setObjectName('prune') dlg.setRunButtonText(_('&Prune')) cw = PruneWidget(repoagent, dlg) cw.setRevset(revspec) dlg.setCommandWidget(cw) return dlg tortoisehg-4.5.2/tortoisehg/hgqt/backout.py0000644000175000017500000004677313153775104021706 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( QSettings, QSize, Qt, pyqtSlot, ) from .qtgui import ( QAction, QCheckBox, QHBoxLayout, QLabel, QProgressBar, QVBoxLayout, QWizard, QWizardPage, ) from ..util import ( hglib, i18n, ) from ..util.i18n import _ from . import ( cmdcore, cmdui, csinfo, qscilib, qtlib, messageentry, resolve, status, thgrepo, wctxcleaner, ) def checkrev(repo, rev): op1, op2 = repo.dirstate.parents() if op1 is None: return _('Backout requires a parent revision') bctx = repo[rev] a = repo.changelog.ancestor(op1, bctx.node()) if a != bctx.node(): return _('Cannot backout change on a different branch') class BackoutDialog(QWizard): def __init__(self, repoagent, rev, parent=None): super(BackoutDialog, self).__init__(parent) self._repoagent = repoagent f = self.windowFlags() self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint) repo = repoagent.rawRepo() parentbackout = repo[rev] == repo['.'] self.setWindowTitle(_('Backout - %s') % repoagent.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, rev, parentbackout, self)) self.addPage(BackoutPage(repoagent, rev, parentbackout, self)) self.addPage(CommitPage(repoagent, rev, parentbackout, 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) self._readSettings() def _readSettings(self): qs = QSettings() qs.beginGroup('backout') for n in ['autoadvance', 'skiplast']: self.setField(n, qs.value(n, False)) repo = self._repoagent.rawRepo() n = 'autoresolve' self.setField(n, repo.ui.configbool('tortoisehg', n, qtlib.readBool(qs, n, True))) qs.endGroup() def _writeSettings(self): qs = QSettings() qs.beginGroup('backout') for n in ['autoadvance', 'autoresolve', 'skiplast']: qs.setValue(n, self.field(n)) qs.endGroup() @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() def done(self, r): self._writeSettings() super(BackoutDialog, self).done(r) 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, backoutrev, parentbackout, 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) 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 bctx = repo[backoutrev] pctx = repo['.'] if parentbackout: lbl = _('Backing out a parent revision is a single step operation') self.layout().addWidget(QLabel(u'%s' % lbl)) ## 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 ## 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')) self.registerField('autoresolve', autoresolve_chk) self.layout().addWidget(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) 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, backoutrev, parentbackout, parent): super(BackoutPage, self).__init__(repoagent, parent) self._backoutrev = backoutrev self._parentbackout = parentbackout self.backoutcomplete = False self.setTitle(_('Backing out, then merging...')) self.setSubTitle(_('All conflicting files will be marked unresolved.')) self.setLayout(QVBoxLayout()) self._cmdlog = cmdui.LogWidget(self) self.layout().addWidget(self._cmdlog) self.reslabel = QLabel() self.reslabel.linkActivated.connect(self.onLinkActivated) self.reslabel.setWordWrap(True) self.layout().addWidget(self.reslabel) autonext = QCheckBox(_('Automatically advance to next page ' 'when backout and merge are complete.')) autonext.clicked.connect(self.tryAutoAdvance) self.registerField('autoadvance', autonext) self.layout().addWidget(autonext) def currentPage(self): if self._parentbackout: self.wizard().next() return tool = self.field('autoresolve') and ':merge' or ':fail' cmdline = hglib.buildcmdargs('backout', self._backoutrev, tool=tool, no_commit=True) self._cmdlog.clearLog() sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self.onCommandFinished) sess.outputReceived.connect(self._cmdlog.appendLog) 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 @pyqtSlot(bool) def tryAutoAdvance(self, checked): if checked and self.isComplete(): self.wizard().next() @pyqtSlot(int) def onCommandFinished(self, ret): if ret in (0, 1): self.backoutcomplete = True if self.field('autoadvance'): self.tryAutoAdvance(True) self.completeChanged.emit() @pyqtSlot(str) def onLinkActivated(self, cmd): if cmd == 'resolve': dlg = resolve.ResolveDialog(self._repoagent, self) dlg.exec_() if self.field('autoadvance'): self.tryAutoAdvance(True) self.completeChanged.emit() class CommitPage(BasePage): def __init__(self, repoagent, backoutrev, parentbackout, parent): super(CommitPage, self).__init__(repoagent, parent) self._backoutrev = backoutrev self._parentbackout = parentbackout 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 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._cmdsession = cmdcore.nullCmdSession() self._cmdlog = cmdui.LogWidget(self) self._cmdlog.hide() self.layout().addWidget(self._cmdlog) 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) skiplast = QCheckBox(_('Skip final confirmation page, ' 'close after commit.')) self.registerField('skiplast', skiplast) self.layout().addWidget(skiplast) def eng_toggled(checked): if self.isComplete(): oldmsg = self.msgEntry.text() 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._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() self.msgEntry.saveSettings(s, 'backout/message') def currentPage(self): engmsg = self.repo.ui.configbool('tortoisehg', 'engmsg', False) msgset = i18n.keepgettext()._('Backed out changeset: ') msg = engmsg and msgset['id'] or msgset['str'] self.msgEntry.setText(msg + str(self.repo[self._backoutrev])) self.msgEntry.moveCursorToEnd() @pyqtSlot(str) 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.field('skiplast'): self.wizard().close() return True if not self._cmdsession.isFinished(): return False user = hglib.tounicode(qtlib.getCurrentUsername(self, self.repo)) if not user: return False if self._parentbackout: self.setTitle(_('Backing out and committing...')) self.setSubTitle(_('Please wait while making backout.')) message = unicode(self.msgEntry.text()) cmdline = hglib.buildcmdargs('backout', self._backoutrev, verbose=True, message=message, user=user) else: self.setTitle(_('Committing...')) self.setSubTitle(_('Please wait while committing merged files.')) message = unicode(self.msgEntry.text()) cmdline = hglib.buildcmdargs('commit', verbose=True, message=message, user=user) commandlines = [cmdline] pushafter = self.repo.ui.config('tortoisehg', 'cipushafter') if pushafter: cmd = ['push', hglib.tounicode(pushafter)] commandlines.append(cmd) self._cmdlog.show() sess = self._repoagent.runCommandSequence(commandlines, self) self._cmdsession = sess sess.commandFinished.connect(self.onCommandFinished) sess.outputReceived.connect(self._cmdlog.appendLog) return False @pyqtSlot(int) def onCommandFinished(self, ret): 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-4.5.2/tortoisehg/hgqt/repofilter.py0000644000175000017500000004524713251112734022415 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os from .qtcore import ( QEvent, QTimer, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QCheckBox, QComboBox, QCompleter, QFontMetrics, QIcon, QLabel, QLineEdit, QMainWindow, QMenu, QSizePolicy, QStyle, QToolBar, QToolButton, ) from mercurial import ( error, repoview, util, ) from ..util import hglib from ..util.i18n import _ from . import ( qtlib, revset, ) _permanent_queries = ('head()', 'merge()', 'tagged()', 'bookmark()', 'file(".hgsubstate") or file(".hgsub")') def _firstword(query): lquery = hglib.fromunicode(query) try: for token, value, _pos in hglib.tokenizerevspec(lquery): if token == 'symbol' or token == 'string': return value # localstr except error.ParseError: pass def _querytype(repo, query): r""" >>> 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' >>> _querytype(repo, u'\u3000') # UnicodeEncodeError 'revset' """ if not query: return if '(' in query: return 'revset' try: changeid = _firstword(query) except UnicodeEncodeError: return 'revset' # avoid further error on formatspec() if not changeid: return 'keyword' try: if changeid in repo: return 'revset' except error.LookupError: # ambiguous changeid pass return 'keyword' class SelectAllLineEdit(QLineEdit): def __init__(self, parent=None): super(SelectAllLineEdit, self).__init__(parent) self.just_got_focus = False def focusInEvent(self, ev): if ev.reason() == Qt.MouseFocusReason: self.just_got_focus = True super(SelectAllLineEdit, self).focusInEvent(ev) def mouseReleaseEvent(self, ev): if self.just_got_focus: self.just_got_focus = False self.selectAll() super(SelectAllLineEdit, self).mouseReleaseEvent(ev) class RepoFilterBar(QToolBar): """Toolbar for RepoWidget to filter changesets""" setRevisionSet = pyqtSignal(str) filterToggled = pyqtSignal(bool) branchChanged = pyqtSignal(str, bool) """Emitted (branch, allparents) when branch selection changed""" showHiddenChanged = pyqtSignal(bool) showGraftSourceChanged = pyqtSignal(bool) _allBranchesLabel = u'\u2605 ' + _('Show all') + u' \u2605' def __init__(self, repoagent, parent=None): super(RepoFilterBar, self).__init__(parent) self.layout().setContentsMargins(0, 0, 0, 0) self.setIconSize(qtlib.smallIconSize()) self._repoagent = repoagent self._permanent_queries = list(_permanent_queries) repo = repoagent.rawRepo() username = hglib.configuredusername(repo.ui) if username: self._permanent_queries.insert(0, hglib.formatrevspec('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(u'\u2605'): self._allBranchesLabel = u'*** %s ***' % _('Show all') self.entrydlg = revset.RevisionSetQuery(repoagent, self) self.entrydlg.queryIssued.connect(self.queryIssued) self.entrydlg.hide() self.revsetcombo = combo = QComboBox() combo.setEditable(True) combo.setInsertPolicy(QComboBox.NoInsert) # don't calculate size hint from history contents, just use as much # space as possible. this way, the branch combo can be enlarged up # to its preferred width. combo.setSizeAdjustPolicy( QComboBox.AdjustToMinimumContentsLengthWithIcon) combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) combo.setMinimumContentsLength(20) 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('hg-remove')) 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 = self.style().standardIcon(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.clicked.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.setFocusProxy(self.revsetcombo) self.refresh() @property def _repo(self): return self._repoagent.rawRepo() def onClearButtonClicked(self): if self.revsetcombo.currentText(): self.revsetcombo.clearEditText() elif not isinstance(self.parentWidget(), QMainWindow): # act as "close" button because this isn't managed as toolbar self.hide() # always request to clear filter; model may still be filtered even # if edit box is empty self.runQuery() 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) if not util.safehasattr(QToolBar, 'visibilityChanged'): # Qt < 4.7 visibilityChanged = pyqtSignal(bool) def event(self, event): etype = event.type() if etype == QEvent.Show or etype == QEvent.Hide and self.isHidden(): self.visibilityChanged.emit(etype == QEvent.Show) return super(RepoFilterBar, self).event(event) 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.setVisible(True) def queryIssued(self, query): self.revsetcombo.setEditText(query) self.runQuery() def _prepareQuery(self): query = unicode(self.revsetcombo.currentText()).strip() if _querytype(self._repo, query) == 'keyword': return hglib.formatrevspec('keyword(%s)', query) else: return query def _isUnsavedQuery(self): return unicode(self.revsetcombo.currentText()).startswith(' ') @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] if self._isUnsavedQuery(): name += ' ' + _('(unsaved)') 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.setCurrentIndex(self.revsetcombo.findText(query)) self.revsetcombo.setEditText(query) @pyqtSlot() def runQuery(self): 'Run the current revset query or request to clear the previous result' query = self._prepareQuery() self.setRevisionSet.emit(query) if query: self.saveQuery() self.revsetcombo.lineEdit().selectAll() def saveQuery(self): query = self.revsetcombo.currentText() if self._isUnsavedQuery(): return 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 = hglib.shortrepoid(self._repo) s.beginGroup('revset/' + repoid) self.entrydlg.restoreGeometry(qtlib.readByteArray(s, 'geom')) self.revsethist = list(qtlib.readStringList(s, 'queries')) self.filtercb.setChecked(qtlib.readBool(s, 'filter', True)) full = self.revsethist + self._permanent_queries self.revsetcombo.clear() self.revsetcombo.addItems(full) self.revsetcombo.setCurrentIndex(-1) self.setVisible(qtlib.readBool(s, 'showrepofilterbar')) self.showHiddenBtn.setChecked(qtlib.readBool(s, 'showhidden')) self.showGraftSourceBtn.setChecked( qtlib.readBool(s, 'showgraftsource', True)) self._loadBranchFilterSettings(s) s.endGroup() def saveSettings(self, s): try: repoid = hglib.shortrepoid(self._repo) 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.InstantPopup, statusTip=_('Display graph the named branch only')) 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.setEditable(True) self._branchCombo.setInsertPolicy(QComboBox.NoInsert) self._branchCombo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self._branchCombo.setLineEdit(SelectAllLineEdit()) self._branchCombo.lineEdit().editingFinished.connect( self._lineBranchChanged) self._branchCombo.setMinimumContentsLength(10) self._branchCombo.setMaxVisibleItems(30) self._branchCombo.currentIndexChanged.connect(self._emitBranchChanged) completer = QCompleter(self._branchCombo.model(), self._branchCombo) self._branchCombo.setCompleter(completer) self.addWidget(self._branchLabel) self.addWidget(qtlib.Spacer(2, 2)) self.addWidget(self._branchCombo) def _loadBranchFilterSettings(self, s): branch = qtlib.readString(s, 'branch') if branch == '.': branch = hglib.tounicode(self._repo.dirstate.branch()) self._branchCombo.blockSignals(True) self.setBranch(branch) self._branchCombo.blockSignals(False) self._allparAction.setChecked(qtlib.readBool(s, 'branch_allparents')) 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.branchmap()) else: branches = hglib.namedbranches(self._repo) # 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)) branches = map(hglib.tounicode, branches) self._branchCombo.blockSignals(True) self._branchCombo.clear() self._branchCombo.addItem(self._allBranchesLabel) self._branchCombo.setItemData(self._branchCombo.count() - 1, '') for i, branch in enumerate(branches, self._branchCombo.count()): self._branchCombo.addItem(branch) self._branchCombo.setItemData(i, branch, Qt.ToolTipRole) self._branchCombo.setItemData(i, branch) self._branchCombo.setEnabled(self.filterEnabled and bool(branches)) self.setBranch(curbranch) self._branchCombo.blockSignals(False) if self.branch() != curbranch: self._emitBranchChanged() # falls back to "show all" @pyqtSlot(str) def setBranch(self, branch): """Change the current branch by name [unicode]""" index = self._branchCombo.findData(branch) if index >= 0: self._branchCombo.setCurrentIndex(index) def branch(self): """Return the current branch name [unicode]""" index = self._branchCombo.currentIndex() branch = self._branchCombo.itemData(index) return unicode(branch) 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 _lineBranchChanged(self): written_branch = self._branchCombo.currentText() if not written_branch: self._branchCombo.setCurrentIndex(0) @pyqtSlot() def refresh(self): self._updateBranchFilter() self._updateShowHiddenBtnState() def _updateShowHiddenBtnState(self): hashidden = bool(repoview.filterrevs(self._repo, 'visible')) self.showHiddenBtn.setEnabled(hashidden) tortoisehg-4.5.2/tortoisehg/hgqt/fileview.py0000644000175000017500000015071513153775104022060 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import cPickle as pickle import difflib import os import re from . import qsci as Qsci from .qtcore import ( QEvent, QObject, QPoint, QSettings, QTime, QTimer, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAction, QActionGroup, QApplication, QColor, QFontMetrics, QFrame, QInputDialog, QKeySequence, QLabel, QPalette, QShortcut, QStyle, QToolBar, QHBoxLayout, QVBoxLayout, ) from mercurial import util from ..util import ( colormap, hglib, ) from ..util.i18n import _ from . import ( blockmatcher, cmdcore, filedata, fileencoding, lexers, qscilib, qtlib, visdiff, ) qsci = qscilib.Scintilla # _NullMode is the fallback mode to display error message or repository history _NullMode = 0 DiffMode = 1 FileMode = 2 AnnMode = 3 _LineNumberMargin = 1 _AnnotateMargin = 2 _ChunkSelectionMargin = 4 _ChunkStartMarker = 0 _IncludedChunkStartMarker = 1 _ExcludedChunkStartMarker = 2 _InsertedLineMarker = 3 _ReplacedLineMarker = 4 _ExcludedLineMarker = 5 _FirstAnnotateLineMarker = 6 # to 31 _ChunkSelectionMarkerMask = ( (1 << _IncludedChunkStartMarker) | (1 << _ExcludedChunkStartMarker)) class HgFileView(QFrame): "file diff, content, and annotation viewer" linkActivated = pyqtSignal(str) fileDisplayed = pyqtSignal(str, str) showMessage = pyqtSignal(str) revisionSelected = pyqtSignal(int) shelveToolExited = pyqtSignal() chunkSelectionChanged = pyqtSignal() grepRequested = pyqtSignal(str, 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() 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(qtlib.smallIconSize()) 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 = qscilib.Scintilla(self) hbox.addWidget(self.blk) hbox.addWidget(self.sci, 1) hbox.addWidget(self.blksearch) self.sci.cursorPositionChanged.connect(self._updateDiffActions) self.sci.setContextMenuPolicy(Qt.CustomContextMenu) self.sci.customContextMenuRequested.connect(self._onMenuRequested) 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) self.sci.installEventFilter(qscilib.KeyPressInterceptor(self)) self.sci.setCaretLineVisible(False) self.sci.markerDefine(qsci.Invisible, _ChunkStartMarker) # hide margin 0 (markers) self.sci.setMarginType(0, qsci.SymbolMargin) self.sci.setMarginWidth(0, 0) self.searchbar = qscilib.SearchToolBar() self.searchbar.hide() self.searchbar.searchRequested.connect(self.find) self.searchbar.conditionChanged.connect(self.highlightText) self.addActions(self.searchbar.editorActions()) self.layout().addWidget(self.searchbar) self._fd = self._nullfd = filedata.createNullData(repo) self._lostMode = _NullMode self._lastSearch = u'', False self._modeToggleGroup = QActionGroup(self) self._modeToggleGroup.triggered.connect(self._setModeByAction) self._modeActionMap = {} for mode, icon, tooltip in [ (DiffMode, 'view-diff', _('View change as unified diff ' 'output')), (FileMode, 'view-file', _('View change in context of file')), (AnnMode, 'view-annotate', _('Annotate with revision numbers')), (_NullMode, '', '')]: if icon: a = self._modeToggleGroup.addAction(qtlib.geticon(icon), '') else: a = self._modeToggleGroup.addAction('') self._modeActionMap[mode] = a a.setCheckable(True) a.setData(mode) a.setToolTip(tooltip) diffc = _DiffViewControl(self.sci, self) diffc.chunkMarkersBuilt.connect(self._updateDiffActions) filec = _FileViewControl(repo.ui, self.sci, self.blk, self) filec.chunkMarkersBuilt.connect(self._updateDiffActions) messagec = _MessageViewControl(self.sci, self) messagec.forceDisplayRequested.connect(self._forceDisplayFile) annotatec = _AnnotateViewControl(repoagent, self.sci, self._fd, self) annotatec.showMessage.connect(self.showMessage) annotatec.editSelectedRequested.connect(self._editSelected) annotatec.grepRequested.connect(self.grepRequested) annotatec.searchSelectedTextRequested.connect(self._searchSelectedText) annotatec.setSourceRequested.connect(self._setSource) annotatec.visualDiffRevisionRequested.connect(self._visualDiffRevision) annotatec.visualDiffToLocalRequested.connect(self._visualDiffToLocal) chunkselc = _ChunkSelectionViewControl(self.sci, self._fd, self) chunkselc.chunkSelectionChanged.connect(self.chunkSelectionChanged) self._activeViewControls = [] self._modeViewControlsMap = { DiffMode: [diffc], FileMode: [filec], AnnMode: [filec, annotatec], _NullMode: [messagec], } self._chunkSelectionViewControl = chunkselc # enabled as necessary # Next/Prev diff (in full file mode) self.actionNextDiff = a = QAction(qtlib.geticon('go-down'), _('Next Diff'), self) a.setShortcut('Alt+Down') a.setToolTip('%s (%s)' % (a.text(), a.shortcut().toString())) a.triggered.connect(self._nextDiff) self.actionPrevDiff = a = QAction(qtlib.geticon('go-up'), _('Previous Diff'), self) a.setShortcut('Alt+Up') a.setToolTip('%s (%s)' % (a.text(), a.shortcut().toString())) a.triggered.connect(self._prevDiff) self._parentToggleGroup = QActionGroup(self) self._parentToggleGroup.triggered.connect(self._setParentRevision) for text in '12': a = self._parentToggleGroup.addAction(text) a.setCheckable(True) a.setShortcut('Ctrl+Shift+%s' % text) 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._onSearchbarTriggered) qtlib.newshortcutsforstdkey(QKeySequence.Find, self, self._showSearchbar) self.actionShelf = QAction('Shelve', self) self.actionShelf.setIcon(qtlib.geticon('hg-shelve')) self.actionShelf.setToolTip(_('Open shelve tool')) self.actionShelf.setVisible(False) self.actionShelf.triggered.connect(self._launchShelve) self._actionAutoTextEncoding = a = QAction(_('&Auto Detect'), self) a.setCheckable(True) self._textEncodingGroup = fileencoding.createActionGroup(self) self._textEncodingGroup.triggered.connect(self._applyTextEncoding) tb = self.diffToolbar tb.addActions(self._parentToggleGroup.actions()) tb.addSeparator() tb.addActions(self._modeToggleGroup.actions()[:-1]) tb.addSeparator() tb.addAction(self.actionNextDiff) tb.addAction(self.actionPrevDiff) tb.addAction(filec.gotoLineAction()) tb.addSeparator() tb.addAction(self.actionFind) tb.addAction(self.actionShelf) self._clearMarkup() self._changeEffectiveMode(_NullMode) repoagent.configChanged.connect(self._applyRepoConfig) self._applyRepoConfig() @property def repo(self): return self._repoagent.rawRepo() @pyqtSlot() def _launchShelve(self): from tortoisehg.hgqt import shelve # TODO: pass self._fd.canonicalFilePath() dlg = shelve.ShelveDialog(self._repoagent, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() self.shelveToolExited.emit() def setShelveButtonVisible(self, visible): self.actionShelf.setVisible(visible) def loadSettings(self, qs, prefix): self.sci.loadSettings(qs, prefix) self._actionAutoTextEncoding.setChecked( qtlib.readBool(qs, prefix + '/autotextencoding', True)) enc = qtlib.readString(qs, prefix + '/textencoding') if enc: try: # prefer repository-specific encoding if specified enc = fileencoding.contentencoding(self.repo.ui, enc) except LookupError: enc = '' if enc: self._changeTextEncoding(enc) def saveSettings(self, qs, prefix): self.sci.saveSettings(qs, prefix) qs.setValue(prefix + '/autotextencoding', self._autoTextEncoding()) qs.setValue(prefix + '/textencoding', self._textEncoding()) @pyqtSlot() def _applyRepoConfig(self): self.sci.setIndentationWidth(self.repo.tabwidth) self.sci.setTabWidth(self.repo.tabwidth) enc = fileencoding.contentencoding(self.repo.ui, self._textEncoding()) self._changeTextEncoding(enc) def isChangeSelectionEnabled(self): chunkselc = self._chunkSelectionViewControl controls = self._modeViewControlsMap[DiffMode] return chunkselc in controls 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 if self.isChangeSelectionEnabled() == bool(enable): return chunkselc = self._chunkSelectionViewControl controls = self._modeViewControlsMap[DiffMode] if enable: controls.append(chunkselc) else: controls.remove(chunkselc) if self._effectiveMode() == DiffMode: self._changeEffectiveMode(DiffMode) @pyqtSlot(QAction) def _setModeByAction(self, action): 'One of the mode toolbar buttons has been toggled' mode = action.data() self._lostMode = _NullMode self._changeEffectiveMode(mode) self._displayLoaded(self._fd) def _effectiveMode(self): a = self._modeToggleGroup.checkedAction() return a.data() def _changeEffectiveMode(self, mode): self._modeActionMap[mode].setChecked(True) newcontrols = list(self._modeViewControlsMap[mode]) for c in reversed(self._activeViewControls): if c not in newcontrols: c.close() for c in newcontrols: if c not in self._activeViewControls: c.open() self._activeViewControls = newcontrols def _restrictModes(self, available): 'Disable modes based on content constraints' available.add(_NullMode) for m, a in self._modeActionMap.iteritems(): a.setEnabled(m in available) self._fallBackToAvailableMode() def _fallBackToAvailableMode(self): if self._lostMode and self._modeActionMap[self._lostMode].isEnabled(): self._changeEffectiveMode(self._lostMode) self._lostMode = _NullMode return curmode = self._effectiveMode() if curmode and self._modeActionMap[curmode].isEnabled(): return fallbackmode = iter(a.data() for a in self._modeToggleGroup.actions() if a.isEnabled()).next() if not self._lostMode: self._lostMode = curmode self._changeEffectiveMode(fallbackmode) def _modeAction(self, mode): if not mode: raise ValueError('null mode cannot be set explicitly') try: return self._modeActionMap[mode] except KeyError: raise ValueError('invalid mode: %r' % mode) def setMode(self, mode): """Switch view to DiffMode/FileMode/AnnMode if available for the current content; otherwise it will be switched later""" action = self._modeAction(mode) if action.isEnabled(): if not action.isChecked(): action.trigger() # implies _setModeByAction() else: self._lostMode = mode @pyqtSlot(QAction) def _setParentRevision(self, action): fd = self._fd ctx = fd.rawContext() pctx = {'1': ctx.p1, '2': ctx.p2}[str(action.text())]() self.display(fd.createRebased(pctx)) def _updateFileDataActions(self): fd = self._fd ctx = fd.rawContext() parents = ctx.parents() ismerge = len(parents) == 2 self._parentToggleGroup.setVisible(ismerge) tooltips = [_('Show changes from first parent'), _('Show changes from second parent')] for a, pctx, tooltip in zip(self._parentToggleGroup.actions(), parents, tooltips): firstline = hglib.longsummary(pctx.description()) a.setToolTip('%s:\n%s [%d:%s] %s' % (tooltip, hglib.tounicode(pctx.branch()), pctx.rev(), pctx, firstline)) a.setChecked(fd.baseRev() == pctx.rev()) def _autoTextEncoding(self): return self._actionAutoTextEncoding.isChecked() def _textEncoding(self): return fileencoding.checkedActionName(self._textEncodingGroup) @pyqtSlot() def _applyTextEncoding(self): self._fd.setTextEncoding(self._textEncoding()) self._displayLoaded(self._fd) def _changeTextEncoding(self, enc): fileencoding.checkActionByName(self._textEncodingGroup, enc) if not self._fd.isNull(): self._applyTextEncoding() @pyqtSlot(str, int, int) def _setSource(self, path, rev, line): # BUG: not work for subrepo self.revisionSelected.emit(rev) ctx = self.repo[rev] fd = filedata.createFileData(ctx, ctx.p1(), hglib.fromunicode(path)) self.display(fd) self.showLine(line) def showLine(self, line): if line < self.sci.lines(): self.sci.setCursorPosition(line, 0) def _moveAndScrollToLine(self, line): self.sci.setCursorPosition(line, 0) self.sci.verticalScrollBar().setValue(line) def filePath(self): return self._fd.filePath() @pyqtSlot() def clearDisplay(self): self._displayLoaded(self._nullfd) def _clearMarkup(self): self.sci.clear() self.sci.clearMarginText() self.sci.markerDeleteAll() 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._updateDiffActions() self.maxWidth = 0 self.sci.showHScrollBar(False) @pyqtSlot() def _forceDisplayFile(self): self._fd.load(self.isChangeSelectionEnabled(), force=True) self._displayLoaded(self._fd) def display(self, fd): if not fd.isLoaded(): fd.load(self.isChangeSelectionEnabled()) fd.setTextEncoding(self._textEncoding()) if self._autoTextEncoding(): fd.detectTextEncoding() fileencoding.checkActionByName(self._textEncodingGroup, fd.textEncoding()) self._displayLoaded(fd) def _displayLoaded(self, fd): if self._fd.filePath() == fd.filePath(): # 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._updateDisplay(fd) # 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) def _updateDisplay(self, fd): self._fd = fd self._clearMarkup() self._updateFileDataActions() if fd.elabel: self.extralabel.setText(fd.elabel) self.extralabel.show() else: self.extralabel.hide() self.filenamelabel.setText(fd.flabel) availablemodes = set() if fd.isValid(): if fd.diff: availablemodes.add(DiffMode) if fd.contents: availablemodes.add(FileMode) if (fd.contents and (fd.rev() is None or fd.rev() >= 0) and fd.fileStatus() != 'R'): availablemodes.add(AnnMode) self._restrictModes(availablemodes) for c in self._activeViewControls: c.display(fd) self.highlightText(*self._lastSearch) self.fileDisplayed.emit(fd.filePath(), fd.fileText()) self.blksearch.syncPageStep() 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() @pyqtSlot(str, bool, bool, bool) def find(self, exp, icase=True, wrap=False, forward=True): self.sci.find(exp, icase, wrap, forward) @pyqtSlot(str, 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 _onSearchbarTriggered(self, checked): if checked: self._loadSelectionIntoSearchbar() @pyqtSlot() def _showSearchbar(self): self._loadSelectionIntoSearchbar() self.searchbar.show() @pyqtSlot() def _searchSelectedText(self): self.searchbar.search(self.sci.selectedText()) self.searchbar.show() def verticalScrollBar(self): return self.sci.verticalScrollBar() def _findNextChunk(self): mask = 1 << _ChunkStartMarker line = self.sci.getCursorPosition()[0] return self.sci.markerFindNext(line + 1, mask) def _findPrevChunk(self): mask = 1 << _ChunkStartMarker line = self.sci.getCursorPosition()[0] return self.sci.markerFindPrevious(line - 1, mask) @pyqtSlot() def _nextDiff(self): line = self._findNextChunk() if line >= 0: self._moveAndScrollToLine(line) @pyqtSlot() def _prevDiff(self): line = self._findPrevChunk() if line >= 0: self._moveAndScrollToLine(line) @pyqtSlot() def _updateDiffActions(self): self.actionNextDiff.setEnabled(self._findNextChunk() >= 0) self.actionPrevDiff.setEnabled(self._findPrevChunk() >= 0) @pyqtSlot(str, int, int) 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.sci.selectedText()) qtlib.editfiles(self.repo, files, line, pattern, self) def _visualDiff(self, path, **opts): path = hglib.fromunicode(path) dlg = visdiff.visualdiff(self.repo.ui, self.repo, [path], opts) if dlg: dlg.exec_() @pyqtSlot(str, int) def _visualDiffRevision(self, path, rev): self._visualDiff(path, change=rev) @pyqtSlot(str, int) def _visualDiffToLocal(self, path, rev): self._visualDiff(path, rev=[str(rev)]) @pyqtSlot(QPoint) def _onMenuRequested(self, point): menu = self._createContextMenu(point) menu.exec_(self.sci.viewport().mapToGlobal(point)) menu.setParent(None) def _createContextMenu(self, point): menu = self.sci.createEditorContextMenu() m = menu.addMenu(_('E&ncoding')) m.addAction(self._actionAutoTextEncoding) m.addSeparator() fileencoding.addActionsToMenu(m, self._textEncodingGroup) line = self.sci.lineNearPoint(point) selection = self.sci.selectedText() def sreq(**opts): return lambda: self.grepRequested.emit(selection, opts) if self._effectiveMode() != AnnMode: if selection: menu.addSeparator() menu.addAction(_('&Search in Current File'), self._searchSelectedText) menu.addAction(_('Search in All &History'), sreq(all=True)) for c in self._activeViewControls: c.setupContextMenu(menu, line) return menu def resizeEvent(self, event): super(HgFileView, self).resizeEvent(event) self._updateScrollBar() 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 _AbstractViewControl(QObject): """Provide the mode-specific view in HgFileView""" def open(self): raise NotImplementedError def close(self): raise NotImplementedError def display(self, fd): raise NotImplementedError def setupContextMenu(self, menu, line): pass _diffHeaderRegExp = re.compile("^@@ -[0-9]+,[0-9]+ \+[0-9]+,[0-9]+ @@") class _DiffViewControl(_AbstractViewControl): """Display the unified diff in HgFileView""" chunkMarkersBuilt = pyqtSignal() def __init__(self, sci, parent=None): super(_DiffViewControl, self).__init__(parent) self._sci = sci self._buildtimer = QTimer(self) self._buildtimer.timeout.connect(self._buildMarker) self._linestoprocess = [] self._firstlinetoprocess = 0 def open(self): self._sci.markerDefine(qsci.Background, _ChunkStartMarker) if qtlib.isDarkTheme(self._sci.palette()): self._sci.setMarkerBackgroundColor(QColor('#204820'), _ChunkStartMarker) else: self._sci.setMarkerBackgroundColor(QColor('#B0FFA0'), _ChunkStartMarker) self._sci.setLexer(lexers.difflexer(self)) def close(self): self._sci.markerDefine(qsci.Invisible, _ChunkStartMarker) self._sci.setLexer(None) self._buildtimer.stop() def display(self, fd): self._sci.setText(fd.diffText()) self._startBuildMarker() def _startBuildMarker(self): self._linestoprocess = unicode(self._sci.text()).splitlines() self._firstlinetoprocess = 0 self._buildtimer.start() @pyqtSlot() def _buildMarker(self): self._sci.setUpdatesEnabled(False) # Process linesPerBlock lines at a time linesPerBlock = 100 # Look for lines matching the "diff header" for n, line in enumerate(self._linestoprocess[:linesPerBlock]): if _diffHeaderRegExp.match(line): diffLine = self._firstlinetoprocess + n self._sci.markerAdd(diffLine, _ChunkStartMarker) self._linestoprocess = self._linestoprocess[linesPerBlock:] self._firstlinetoprocess += linesPerBlock self._sci.setUpdatesEnabled(True) if not self._linestoprocess: self._buildtimer.stop() self.chunkMarkersBuilt.emit() class _FileViewControl(_AbstractViewControl): """Display the file content with chunk markers in HgFileView""" chunkMarkersBuilt = pyqtSignal() def __init__(self, ui, sci, blk, parent=None): super(_FileViewControl, self).__init__(parent) self._ui = ui self._sci = sci self._blk = blk self._sci.setMarginLineNumbers(_LineNumberMargin, True) self._sci.setMarginWidth(_LineNumberMargin, 0) # define markers for colorize zones of diff self._sci.markerDefine(qsci.Background, _InsertedLineMarker) self._sci.markerDefine(qsci.Background, _ReplacedLineMarker) if qtlib.isDarkTheme(self._sci.palette()): self._sci.setMarkerBackgroundColor(QColor('#204820'), _InsertedLineMarker) self._sci.setMarkerBackgroundColor(QColor('#202050'), _ReplacedLineMarker) else: self._sci.setMarkerBackgroundColor(QColor('#B0FFA0'), _InsertedLineMarker) self._sci.setMarkerBackgroundColor(QColor('#A0A0FF'), _ReplacedLineMarker) self._actionGotoLine = a = QAction(qtlib.geticon('go-jump'), _('Go to Line'), self) a.setEnabled(False) a.setShortcut('Ctrl+J') a.setToolTip('%s (%s)' % (a.text(), a.shortcut().toString())) a.triggered.connect(self._gotoLineDialog) self._buildtimer = QTimer(self) self._buildtimer.timeout.connect(self._buildMarker) self._opcodes = [] def open(self): self._blk.setVisible(True) self._actionGotoLine.setEnabled(True) def close(self): self._blk.setVisible(False) self._sci.setMarginWidth(_LineNumberMargin, 0) self._sci.setLexer(None) self._actionGotoLine.setEnabled(False) self._buildtimer.stop() def display(self, fd): if fd.contents: filename = hglib.fromunicode(fd.filePath()) lexer = lexers.getlexer(self._ui, filename, fd.contents, self) self._sci.setLexer(lexer) if lexer is None: self._sci.setFont(qtlib.getfont('fontlog').font()) self._sci.setText(fd.fileText()) self._sci.setMarginsFont(self._sci.font()) width = len(str(self._sci.lines())) + 2 # 2 for margin self._sci.setMarginWidth(_LineNumberMargin, 'M' * width) self._blk.syncPageStep() if fd.contents and fd.olddata: self._startBuildMarker(fd) else: self._buildtimer.stop() # in case previous request not finished def _startBuildMarker(self, fd): # use the difflib.SequenceMatcher, which returns a set of opcodes # that must be parsed olddata = fd.olddata.splitlines() newdata = fd.contents.splitlines() diff = difflib.SequenceMatcher(None, olddata, newdata) self._opcodes = diff.get_opcodes() self._buildtimer.start() @pyqtSlot() def _buildMarker(self): self._sci.setUpdatesEnabled(False) self._blk.setUpdatesEnabled(False) for tag, alo, ahi, blo, bhi in self._opcodes[:30]: if tag in ('replace', 'insert'): self._sci.markerAdd(blo, _ChunkStartMarker) if tag == 'replace': self._blk.addBlock('x', blo, bhi) for i in range(blo, bhi): self._sci.markerAdd(i, _ReplacedLineMarker) elif tag == 'insert': self._blk.addBlock('+', blo, bhi) for i in range(blo, bhi): self._sci.markerAdd(i, _InsertedLineMarker) elif tag in ('equal', 'delete'): pass else: raise ValueError, 'unknown tag %r' % (tag,) self._opcodes = self._opcodes[30:] self._sci.setUpdatesEnabled(True) self._blk.setUpdatesEnabled(True) if not self._opcodes: self._buildtimer.stop() self.chunkMarkersBuilt.emit() def gotoLineAction(self): return self._actionGotoLine @pyqtSlot() def _gotoLineDialog(self): last = self._sci.lines() if last == 0: return cur = self._sci.getCursorPosition()[0] + 1 line, ok = QInputDialog.getInt(self.parent(), _('Go to Line'), _('Enter line number (1 - %d)') % last, cur, 1, last) if ok: self._sci.setCursorPosition(line - 1, 0) self._sci.ensureLineVisible(line - 1) self._sci.setFocus() class _MessageViewControl(_AbstractViewControl): """Display error message or repository history in HgFileView""" forceDisplayRequested = pyqtSignal() def __init__(self, sci, parent=None): super(_MessageViewControl, self).__init__(parent) self._sci = sci self._forceviewindicator = None def open(self): self._sci.setLexer(None) self._sci.setFont(qtlib.getfont('fontlog').font()) def close(self): pass def display(self, fd): if not fd.isValid(): errormsg = fd.error or '' self._sci.setText(errormsg) forcedisplaymsg = filedata.forcedisplaymsg linkstart = errormsg.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) elif fd.ucontents: # subrepo summary and perhaps other data self._sci.setText(fd.ucontents) def _setupForceViewIndicator(self): if self._forceviewindicator is not None: return 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._requestForceDisplay, Qt.QueuedConnection) @pyqtSlot() def _requestForceDisplay(self): self._sci.setText(_('Please wait while the file is opened ...')) # Wait a little to ensure that the "wait message" is displayed QTimer.singleShot(10, self.forceDisplayRequested) class _AnnotateViewControl(_AbstractViewControl): """Display annotation margin and colorize file content in HgFileView""" showMessage = pyqtSignal(str) editSelectedRequested = pyqtSignal(str, int, int) grepRequested = pyqtSignal(str, dict) searchSelectedTextRequested = pyqtSignal() setSourceRequested = pyqtSignal(str, int, int) visualDiffRevisionRequested = pyqtSignal(str, int) visualDiffToLocalRequested = pyqtSignal(str, int) def __init__(self, repoagent, sci, fd, parent=None): super(_AnnotateViewControl, self).__init__(parent) self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() self._sci = sci self._sci.setMarginType(_AnnotateMargin, qsci.TextMarginRightJustified) self._sci.setMarginSensitivity(_AnnotateMargin, True) self._sci.marginClicked.connect(self._onMarginClicked) self._fd = fd self._links = [] # by line self._revmarkers = {} # by rev self._lastrev = -1 self._lastmarginclick = QTime.currentTime() self._lastmarginclick.addMSecs(-QApplication.doubleClickInterval()) self._initAnnotateOptionActions() self._loadAnnotateSettings() self._isdarktheme = qtlib.isDarkTheme(self._sci.palette()) def open(self): self._sci.viewport().installEventFilter(self) def close(self): self._sci.viewport().removeEventFilter(self) self._sci.setMarginWidth(_AnnotateMargin, 0) self._sci.markerDeleteAll() self._cmdsession.abort() def eventFilter(self, watched, event): # Python wrapper is deleted immediately before QEvent.Destroy try: sciviewport = self._sci.viewport() except RuntimeError: sciviewport = None if watched is sciviewport: if event.type() == QEvent.MouseMove: line = self._sci.lineNearPoint(event.pos()) self._emitRevisionHintAtLine(line) return False return super(_AnnotateViewControl, self).eventFilter(watched, event) def _loadAnnotateSettings(self): s = QSettings() wb = "Annotate/" for a in self._annoptactions: a.setChecked(qtlib.readBool(s, wb + a.data())) if not any(a.isChecked() for a in self._annoptactions): self._annoptactions[-1].setChecked(True) # 'rev' by default def _saveAnnotateSettings(self): s = QSettings() wb = "Annotate/" for a in self._annoptactions: s.setValue(wb + a.data(), 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 any(a.isChecked() for a in self._annoptactions): self.sender().setChecked(True) self._updateView() self._saveAnnotateSettings() def _buildRevMarginTexts(self): def getauthor(fctx): return hglib.tounicode(hglib.username(fctx.user())) def getdate(fctx): return util.shortdate(fctx.date()) if self._fd.rev() is None: p1rev = self._fd.parentRevs()[0] revfmt = '%%%dd%%c' % len(str(p1rev)) def getrev(fctx): if fctx.rev() is None: return revfmt % (p1rev, '+') else: return revfmt % (fctx.rev(), ' ') else: revfmt = '%%%dd' % len(str(self._fd.rev())) def getrev(fctx): return revfmt % fctx.rev() aformat = [str(a.data()) for a in self._annoptactions if a.isChecked()] annfields = { 'rev': getrev, 'author': getauthor, 'date': getdate, } annfunc = [annfields[n] for n in aformat] uniqfctxs = set(fctx for fctx, _origline in self._links) return dict((fctx.rev(), ' : '.join(f(fctx) for f in annfunc)) for fctx in uniqfctxs) def _emitRevisionHintAtLine(self, line): if line < 0 or line >= len(self._links): return fctx = self._links[line][0] if fctx.rev() != self._lastrev: filename = hglib.fromunicode(self._fd.canonicalFilePath()) s = hglib.get_revision_desc(fctx, filename) self.showMessage.emit(s) self._lastrev = fctx.rev() def _repoAgentForFile(self): rpath = self._fd.repoRootPath() if not rpath: return self._repoagent return self._repoagent.subRepoAgent(rpath) def display(self, fd): if self._fd == fd and self._links: self._updateView() return self._fd = fd del self._links[:] self._cmdsession.abort() repoagent = self._repoAgentForFile() cmdline = hglib.buildcmdargs('annotate', fd.canonicalFilePath(), rev=hglib.escaperev(fd.rev(), 'wdir()'), text=True, file=True, number=True, line_number=True, T='pickle') self._cmdsession = sess = repoagent.runCommand(cmdline, self) sess.setCaptureOutput(True) sess.commandFinished.connect(self._onAnnotateFinished) @pyqtSlot(int) def _onAnnotateFinished(self, ret): sess = self._cmdsession if not sess.isFinished(): # new request is already running return if ret != 0: return repo = self._repoAgentForFile().rawRepo() data = pickle.loads(str(sess.readAll())) links = [] fctxcache = {} # (path, rev): fctx for l in data[0]['lines']: path, rev = l['file'], l['rev'] try: fctx = fctxcache[path, rev] except KeyError: fctx = fctxcache[path, rev] = repo[rev][path] links.append((fctx, l['line_number'])) self._links = links self._updateView() def _updateView(self): if not self._links: return revtexts = self._buildRevMarginTexts() self._updaterevmargin(revtexts) self._updatemarkers() self._updatemarginwidth(revtexts) def _updaterevmargin(self, revtexts): """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._sci.SendScintilla(qsci.SCI_STYLESETBACK, s.style(), s.paper()) self._sci.SendScintilla(qsci.SCI_STYLESETFONT, s.style(), unicode(s.font().family()).encode('latin-1')) self._sci.SendScintilla(qsci.SCI_STYLESETSIZE, s.style(), s.font().pointSize()) for i, (fctx, _origline) in enumerate(self._links): self._sci.setMarginText(i, revtexts[fctx.rev()], 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._sci.markerAdd(i, m) def _redefinemarkers(self): """Redefine line markers according to the current revs""" curdate = self._fd.rawContext().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) maxcolors = 32 - _FirstAnnotateLineMarker palette = colormap.makeannotatepalette(filectxs, curdate, maxcolors=maxcolors, maxhues=8, maxsaturations=16, mindate=mindate, isdarktheme=self._isdarktheme) for i, (color, fctxs) in enumerate(palette.iteritems()): m = _FirstAnnotateLineMarker + i self._sci.markerDefine(qsci.Background, m) self._sci.setMarkerBackgroundColor(QColor(color), m) for fctx in fctxs: self._revmarkers[fctx.rev()] = m @util.propertycache def _margin_style(self): """Style for margin area""" s = Qsci.QsciStyle() s.setPaper(QApplication.palette().color(QPalette.Window)) s.setFont(self._sci.font()) return s def _updatemarginwidth(self, revtexts): self._sci.setMarginsFont(self._sci.font()) # add 2 for margin maxwidth = 2 + max(len(s) for s in revtexts.itervalues()) self._sci.setMarginWidth(_AnnotateMargin, 'M' * maxwidth) def setupContextMenu(self, menu, line): menu.addSeparator() annoptsmenu = menu.addMenu(_('Annotate Op&tions')) annoptsmenu.addActions(self._annoptactions) if line < 0 or line >= len(self._links): return menu.addSeparator() fctx, line = self._links[line] selection = self._sci.selectedText() if selection: def sreq(**opts): return lambda: self.grepRequested.emit(selection, opts) menu.addSeparator() annsearchmenu = menu.addMenu(_('Search Selected Text')) a = annsearchmenu.addAction(_('In Current &File')) a.triggered.connect(self.searchSelectedTextRequested) 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.setSourceRequested.emit(*data) def editorig(): self.editSelectedRequested.emit(*data) def difflocal(): self.visualDiffToLocalRequested.emit(data[0], data[1]) def diffparent(): self.visualDiffRevisionRequested.emit(data[0], data[1]) menu.addSeparator() 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) anndiffmenu.addAction(_('&Parent Revision'), diffparent) for pfctx in fctx.parents(): pdata = [hglib.tounicode(pfctx.path()), pfctx.changectx().rev(), line] def annparent(data): self.setSourceRequested.emit(*data) def editparent(data): self.editSelectedRequested.emit(*data) for name, func, smenu in [(_('&Parent Revision (%d)') % pdata[1], annparent, anngotomenu), (_('&Parent Revision (%d)') % pdata[1], editparent, annviewmenu)]: 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) #@pyqtSlot(int, int, Qt.KeyboardModifiers) def _onMarginClicked(self, margin, line, state): if margin != _AnnotateMargin: return lastclick = self._lastmarginclick if (state == Qt.ControlModifier or lastclick.elapsed() < QApplication.doubleClickInterval()): if line >= len(self._links): # empty line next to the last line return fctx, line = self._links[line] self.setSourceRequested.emit( hglib.tounicode(fctx.path()), fctx.rev(), line) else: lastclick.restart() # mimic the default "border selection" behavior, # which is disabled when you use setMarginSensitivity() if state == Qt.ShiftModifier: r = self._sci.getSelection() sellinetop, selchartop, sellinebottom, selcharbottom = r 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) class _ChunkSelectionViewControl(_AbstractViewControl): """Display chunk selection margin and colorize chunks in HgFileView""" chunkSelectionChanged = pyqtSignal() def __init__(self, sci, fd, parent=None): super(_ChunkSelectionViewControl, self).__init__(parent) self._sci = sci p = qtlib.getcheckboxpixmap(QStyle.State_On, QColor('#B0FFA0'), sci) self._sci.markerDefine(p, _IncludedChunkStartMarker) p = qtlib.getcheckboxpixmap(QStyle.State_Off, QColor('#B0FFA0'), sci) self._sci.markerDefine(p, _ExcludedChunkStartMarker) self._sci.markerDefine(qsci.Background, _ExcludedLineMarker) if qtlib.isDarkTheme(self._sci.palette()): bg, fg = QColor(44, 44, 44), QColor(86, 86, 86) else: bg, fg = QColor('lightgrey'), QColor('darkgrey') self._sci.setMarkerBackgroundColor(bg, _ExcludedLineMarker) self._sci.setMarkerForegroundColor(fg, _ExcludedLineMarker) self._sci.setMarginType(_ChunkSelectionMargin, qsci.SymbolMargin) self._sci.setMarginMarkerMask(_ChunkSelectionMargin, _ChunkSelectionMarkerMask) self._sci.setMarginSensitivity(_ChunkSelectionMargin, True) self._sci.marginClicked.connect(self._onMarginClicked) self._actmarkexcluded = a = QAction(_('&Mark Excluded Changes'), self) a.setCheckable(True) a.setChecked(qtlib.readBool(QSettings(), 'changes-mark-excluded')) a.triggered.connect(self._updateChunkIndicatorMarks) self._excludeindicator = -1 self._updateChunkIndicatorMarks(a.isChecked()) self._sci.setIndicatorDrawUnder(True, self._excludeindicator) self._sci.setIndicatorForegroundColor(QColor('gray'), self._excludeindicator) self._toggleshortcut = a = QShortcut(Qt.Key_Space, sci) a.setContext(Qt.WidgetShortcut) a.setEnabled(False) a.activated.connect(self._toggleCurrentChunk) self._fd = fd self._chunkatline = {} def open(self): self._sci.setMarginWidth(_ChunkSelectionMargin, 15) self._toggleshortcut.setEnabled(True) def close(self): self._sci.setMarginWidth(_ChunkSelectionMargin, 0) self._toggleshortcut.setEnabled(False) def display(self, fd): self._fd = fd self._chunkatline.clear() if not fd.changes: return for chunk in fd.changes.hunks: self._chunkatline[chunk.lineno] = chunk self._updateMarker(chunk) def _updateMarker(self, chunk): excludemsg = ' ' + _('(excluded from the next commit)') # markerAdd() does not check if the specified marker is already # present, but markerDelete() does m = self._sci.markersAtLine(chunk.lineno) inclmarked = m & (1 << _IncludedChunkStartMarker) exclmarked = m & (1 << _ExcludedChunkStartMarker) if chunk.excluded and not exclmarked: self._sci.setReadOnly(False) llen = self._sci.lineLength(chunk.lineno) # in bytes self._sci.insertAt(excludemsg, chunk.lineno, llen - 1) self._sci.setReadOnly(True) self._sci.markerDelete(chunk.lineno, _IncludedChunkStartMarker) self._sci.markerAdd(chunk.lineno, _ExcludedChunkStartMarker) for i in xrange(chunk.linecount - 1): self._sci.markerAdd(chunk.lineno + i + 1, _ExcludedLineMarker) self._sci.fillIndicatorRange(chunk.lineno + 1, 0, chunk.lineno + chunk.linecount, 0, self._excludeindicator) if not chunk.excluded and exclmarked: self._sci.setReadOnly(False) llen = self._sci.lineLength(chunk.lineno) # in bytes mlen = len(excludemsg.encode('utf-8')) # in bytes 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) if not chunk.excluded and not inclmarked: self._sci.markerDelete(chunk.lineno, _ExcludedChunkStartMarker) self._sci.markerAdd(chunk.lineno, _IncludedChunkStartMarker) for i in xrange(chunk.linecount - 1): self._sci.markerDelete(chunk.lineno + i + 1, _ExcludedLineMarker) self._sci.clearIndicatorRange(chunk.lineno + 1, 0, chunk.lineno + chunk.linecount, 0, self._excludeindicator) #@pyqtSlot(int, int, Qt.KeyboardModifier) def _onMarginClicked(self, margin, line, state): if margin != _ChunkSelectionMargin: return if line not in self._chunkatline: return if state & Qt.ShiftModifier: excluded = self._getChunkAtLine(line) cl = self._currentChunkLine() end = max(line, cl) l = min(line, cl) lines = [] while l < end: assert l >= 0 lines.append(l) l = self._sci.markerFindNext(l + 1, _ChunkSelectionMarkerMask) lines.append(end) self._setChunkAtLines(lines, not excluded) else: self._toggleChunkAtLine(line) self._sci.setCursorPosition(line, 0) def _getChunkAtLine(self, line): return self._chunkatline[line].excluded def _setChunkAtLines(self, lines, excluded): for l in lines: chunk = self._chunkatline[l] self._fd.setChunkExcluded(chunk, excluded) self._updateMarker(chunk) self.chunkSelectionChanged.emit() def _toggleChunkAtLine(self, line): excluded = self._getChunkAtLine(line) self._setChunkAtLines([line], not excluded) @pyqtSlot() def _toggleCurrentChunk(self): line = self._currentChunkLine() if line >= 0: self._toggleChunkAtLine(line) def _currentChunkLine(self): line = self._sci.getCursorPosition()[0] return self._sci.markerFindPrevious(line, _ChunkSelectionMarkerMask) def setupContextMenu(self, menu, line): menu.addAction(self._actmarkexcluded) @pyqtSlot(bool) def _updateChunkIndicatorMarks(self, checked): ''' This method has some pre-requisites: - 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[checked], self._excludeindicator) QSettings().setValue('changes-mark-excluded', checked) tortoisehg-4.5.2/tortoisehg/hgqt/hgrcutil.py0000644000175000017500000000327313150123225022047 0ustar sborhosborho00000000000000# 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 import qtlib from tortoisehg.util import wconfig from tortoisehg.util.i18n import _ 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-4.5.2/tortoisehg/hgqt/rejects.py0000644000175000017500000002645413153775104021707 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import cStringIO from . import qsci as Qsci from .qtcore import ( QPoint, QSettings, QTimer, Qt, pyqtSlot, ) from .qtgui import ( QColor, QDialog, QDialogButtonBox, QFontMetrics, QHBoxLayout, QKeySequence, QListWidget, QMessageBox, QToolButton, QVBoxLayout, ) from mercurial import patch from ..util import hglib from ..util.i18n import _ from . import ( fileencoding, lexers, qscilib, qtlib, ) 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)) editor.setContextMenuPolicy(Qt.CustomContextMenu) editor.customContextMenuRequested.connect(self._onMenuRequested) self.baseLineColor = editor.markerDefine(qsci.Background, -1) editor.setMarkerBackgroundColor(QColor('lightblue'), self.baseLineColor) self.layout().addWidget(editor, 3) searchbar = qscilib.SearchToolBar(self) 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.addActions(searchbar.editorActions()) 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) self.textencgroup = fileencoding.createActionGroup(self) self.textencgroup.triggered.connect(self._reloadFile) fileencoding.checkActionByName(self.textencgroup, fileencoding.contentencoding(ui)) 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(qtlib.readByteArray(s, 'rejects/geometry')) self.editor.loadSettings(s, 'rejects/editor') self.rejectbrowser.loadSettings(s, 'rejects/rejbrowse') if not qscilib.readFile(editor, hglib.tounicode(path), self._textEncoding()): self.hide() QTimer.singleShot(0, self.reject) return earlybytes = hglib.fromunicode(editor.text(), 'replace')[:4096] lexer = lexers.getlexer(ui, path, earlybytes, self) editor.setLexer(lexer) if lexer is None: editor.setFont(qtlib.getfont('fontlog').font()) 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 = patch.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)) @pyqtSlot(QPoint) def _onMenuRequested(self, point): menu = self.editor.createStandardContextMenu() menu.addSeparator() m = menu.addMenu(_('E&ncoding')) fileencoding.addActionsToMenu(m, self.textencgroup) menu.exec_(self.editor.viewport().mapToGlobal(point)) menu.setParent(None) 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) chunkstr = buf.getvalue().decode(self._textEncoding(), 'replace') startline = max(chunk.fromline-1, 0) self.rejectbrowser.showChunk(chunkstr.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 _textEncoding(self): return fileencoding.checkedActionName(self.textencgroup) @pyqtSlot() def _reloadFile(self): if self.editor.isModified(): r = qtlib.QuestionMsgBox(_('Reload File'), _('Are you sure you want to reload this ' 'file?'), _('All unsaved changes will be lost.'), parent=self) if not r: return qscilib.readFile(self.editor, hglib.tounicode(self.path), self._textEncoding()) self.showChunk(self.chunklist.currentRow()) 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: if not qscilib.writeFile(self.editor, hglib.tounicode(self.path), self._textEncoding()): 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(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-4.5.2/tortoisehg/hgqt/thgstrip.py0000644000175000017500000001441213150123225022067 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( Qt, pyqtSlot, ) from .qtgui import ( QCheckBox, QComboBox, QGridLayout, QLabel, QSizePolicy, QVBoxLayout, ) from mercurial import error from ..util import hglib from ..util.i18n import _, ngettext from . import ( cmdcore, cmdui, cslist, qtlib, ) class StripWidget(cmdui.AbstractCmdWidget): """Command widget to strip changesets""" def __init__(self, repoagent, rev=None, parent=None, opts={}): super(StripWidget, self).__init__(parent) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._repoagent = repoagent grid = QGridLayout() grid.setContentsMargins(0, 0, 0, 0) grid.setSpacing(6) self.setLayout(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 hglib.namedbranches(self.repo): combo.addItem(hglib.tounicode(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) grid.addWidget(QLabel(_('Options:')), 3, 0, Qt.AlignLeft | Qt.AlignTop) grid.addLayout(optbox, 3, 1) self._optchks = {} for name, text in [ ('force', _('Discard local changes, no backup (-f/--force)')), ('nobackup', _('No backup (-n/--nobackup)')), ('keep', _('Do not modify working copy during strip ' '(-k/--keep)')), ]: self._optchks[name] = w = QCheckBox(text) w.setChecked(bool(opts.get(name))) optbox.addWidget(w) grid.setRowStretch(cslistrow, 1) grid.setColumnStretch(cslistcol, 1) # signal handlers self.rev_combo.editTextChanged.connect(self.preview) # prepare to show self.rev_combo.lineEdit().selectAll() self.cslist.setHidden(False) 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) else: self.cslist.clear() self.cslist.updatestatus() cstext = qtlib.markup(_('Unknown revision!'), fg='red', weight='bold') self.status.setText(cstext) self.commandChanged.emit() def canRunCommand(self): return self.get_rev() is not None def runCommand(self): opts = {'verbose': True} opts.update((n, w.isChecked()) for n, w in self._optchks.iteritems()) wc = self.repo[None] wcparents = wc.parents() wcp1rev = wcparents[0].rev() wcp2rev = None if len(wcparents) > 1: wcp2rev = wcparents[1].rev() if not opts['force'] and not opts['keep'] and \ (wcp1rev in self.cslist.curitems or wcp2rev in self.cslist.curitems) and \ (wc.modified() or wc.added() or wc.removed()): main = _("Detected uncommitted local changes.") text = _("Do you want to keep them or discard them?") choices = (_('&Keep (--keep)'), _('&Discard (--force)'), _('&Cancel'), ) resp = qtlib.CustomPrompt(_('Confirm Strip'), '%s

%s' % (main, text), self, choices, default=0, esc=2).run() if resp == 0: opts['keep'] = True elif resp == 1: opts['force'] = True else: return cmdcore.nullCmdSession() rev = self.rev_combo.currentText() cmdline = hglib.buildcmdargs('strip', rev, **opts) return self._repoagent.runCommand(cmdline, self) def createStripDialog(repoagent, rev=None, parent=None, opts={}): dlg = cmdui.CmdControlDialog(parent) dlg.setWindowIcon(qtlib.geticon('hg-strip')) dlg.setWindowTitle(_('Strip - %s') % repoagent.displayName()) dlg.setObjectName('strip') dlg.setRunButtonText(_('&Strip')) dlg.setCommandWidget(StripWidget(repoagent, rev, dlg, opts)) return dlg tortoisehg-4.5.2/tortoisehg/hgqt/webconf.py0000644000175000017500000003131113150123225021643 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os from .qtcore import ( QAbstractTableModel, QModelIndex, Qt, pyqtSlot, ) from .qtgui import ( QDialog, QDialogButtonBox, QFileDialog, QFontMetrics, QFormLayout, QHBoxLayout, QLineEdit, QStyle, QToolButton, QWidget, ) from ..util import ( hglib, wconfig, ) from ..util.i18n import _ from . import qtlib from .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('hg-add')) self._qui.edit_button.setIcon(qtlib.geticon('edit-file')) self._qui.remove_button.setIcon(qtlib.geticon('hg-remove')) 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) 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, _filter = 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, _filter = 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-4.5.2/tortoisehg/hgqt/rebase.py0000644000175000017500000002555313153775104021510 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import import os from .qtcore import ( QSettings, QTimer, Qt, pyqtSlot, ) from .qtgui import ( QCheckBox, QDialog, QDialogButtonBox, QGroupBox, QLabel, QMessageBox, QVBoxLayout, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, csinfo, qtlib, resolve, thgrepo, wctxcleaner, ) BB = QDialogButtonBox class RebaseDialog(QDialog): 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 self._cmdsession = cmdcore.nullCmdSession() repo = repoagent.rawRepo() self.opts = opts 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) self.swaplabel = QLabel('%s' # don't care href % _('Swap source and destination')) self.swaplabel.linkActivated.connect(self.swap) self.layout().addWidget(self.swaplabel) sep = qtlib.LabeledSeparator(_('Options')) self.layout().addWidget(sep) self.keepchk = QCheckBox(_('Keep original changesets (--keep)')) self.keepchk.setChecked(opts.get('keep', False)) self.layout().addWidget(self.keepchk) self.keepbrancheschk = QCheckBox(_('Keep original branch names ' '(--keepbranches)')) self.keepbrancheschk.setChecked(opts.get('keepbranches', False)) self.layout().addWidget(self.keepbrancheschk) self.collapsechk = QCheckBox(_('Collapse the rebased changesets ' '(--collapse)')) self.collapsechk.setChecked(opts.get('collapse', False)) self.layout().addWidget(self.collapsechk) self.basechk = QCheckBox(_('Rebase entire source branch (-b/--base)')) self.layout().addWidget(self.basechk) self.autoresolvechk = QCheckBox(_('Automatically resolve merge ' 'conflicts where possible')) self.layout().addWidget(self.autoresolvechk) self.svnchk = QCheckBox(_('Rebase unpublished onto Subversion head ' '(override source, destination)')) self.svnchk.setVisible('hgsubversion' in repo.extensions()) self.layout().addWidget(self.svnchk) self._cmdlog = cmdui.LogWidget(self) self._cmdlog.hide() self.layout().addWidget(self._cmdlog, 2) self._stbar = cmdui.ThgStatusBar(self) self._stbar.setSizeGripEnabled(False) self._stbar.linkActivated.connect(self.linkActivated) self.layout().addWidget(self._stbar) 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._cmdlog.show() else: self._stbar.showMessage(_('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') % repoagent.displayName()) self._readSettings() @property def repo(self): return self._repoagent.rawRepo() def _readSettings(self): qs = QSettings() qs.beginGroup('rebase') self.autoresolvechk.setChecked( self.repo.ui.configbool('tortoisehg', 'autoresolve', qtlib.readBool(qs, 'autoresolve', True))) qs.endGroup() def _writeSettings(self): qs = QSettings() qs.beginGroup('rebase') qs.setValue('autoresolve', self.autoresolvechk.isChecked()) qs.endGroup() @pyqtSlot(bool) def _onCheckFinished(self, clean): if not clean: self.rebasebtn.setEnabled(False) txt = _('Before rebase, you must ' 'commit, ' 'shelve to patch, ' 'or discard changes.') else: self.rebasebtn.setEnabled(True) txt = _('You may continue the rebase') self._stbar.showMessage(txt) def rebase(self): self.rebasebtn.setEnabled(False) self.cancelbtn.setVisible(False) self.keepchk.setEnabled(False) self.keepbrancheschk.setEnabled(False) self.basechk.setEnabled(False) self.collapsechk.setEnabled(False) self.swaplabel.setVisible(False) itool = self.autoresolvechk.isChecked() and 'merge' or 'fail' opts = {'config': 'ui.merge=internal:%s' % itool} if os.path.exists(self.repo.vfs.join('rebasestate')): opts['continue'] = True else: opts.update({ 'keep': self.keepchk.isChecked(), 'keepbranches': self.keepbrancheschk.isChecked(), 'collapse': self.collapsechk.isChecked(), }) if self.svnchk.isChecked(): opts['svn'] = True else: sourcearg = 'source' if self.basechk.isChecked(): sourcearg = 'base' opts[sourcearg] = hglib.tounicode(str(self.opts.get('source'))) opts['dest'] = hglib.tounicode(str(self.opts.get('dest'))) cmdline = hglib.buildcmdargs('rebase', **opts) sess = self._runCommand(cmdline) sess.commandFinished.connect(self._rebaseFinished) 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 = hglib.buildcmdargs('rebase', abort=True) sess = self._runCommand(cmdline) sess.commandFinished.connect(self._abortFinished) def _runCommand(self, cmdline): assert self._cmdsession.isFinished() self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self._stbar.clearProgress) sess.outputReceived.connect(self._cmdlog.appendLog) sess.progressReceived.connect(self._stbar.setProgress) cmdui.updateStatusMessage(self._stbar, sess) return sess @pyqtSlot(int) def _rebaseFinished(self, ret): # 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 ret == 255: msg = _('Rebase failed') self._cmdlog.show() # contains hint self._stbar.showMessage(msg) self._makeCloseButton() @pyqtSlot() def _abortFinished(self): if self.checkResolve() is False: self._stbar.showMessage(_('Rebase aborted')) self._makeCloseButton() def _makeCloseButton(self): 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._stbar.showMessage(txt) if os.path.exists(self.repo.vfs.join('rebasestate')): self.swaplabel.setVisible(False) 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.vfs.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() def done(self, r): self._writeSettings() super(RebaseDialog, self).done(r) tortoisehg-4.5.2/tortoisehg/hgqt/manifestmodel.py0000644000175000017500000005260113251112734023061 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import re from .qtcore import ( QAbstractItemModel, QMimeData, QModelIndex, QUrl, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QCompleter, QFileIconProvider, QIcon, ) from mercurial import ( error, match as matchmod, subrepo, ) from ..util import hglib from . import ( filedata, qtlib, status, visdiff, ) _subrepoType2IcoMap = { 'hg': 'hg', 'hgsubversion': 'thg-svn-subrepo', 'git': 'thg-git-subrepo', 'svn': 'thg-svn-subrepo', } _subrepoStatus2IcoMap = { 'A': 'thg-added-subrepo', 'R': 'thg-removed-subrepo', } class ManifestModel(QAbstractItemModel): """Status of files between two revisions or patch""" # emitted when all files of the revision has been loaded successfully revLoaded = pyqtSignal(object) StatusRole = Qt.UserRole + 1 """Role for file change status""" # -1 and None are valid revision number FirstParent = -2 SecondParent = -3 def __init__(self, repoagent, parent=None, rev=None, namefilter=None, statusfilter='MASC', flat=False): QAbstractItemModel.__init__(self, parent) self._fileiconprovider = QFileIconProvider() self._iconcache = {} # (path, status, subkind): icon self._repoagent = repoagent self._namefilter = unicode(namefilter or '') assert all(c in 'MARSC' for c in statusfilter) self._statusfilter = statusfilter self._changedfilesonly = False self._nodeop = _nodeopmap[bool(flat)] self._rootentry = self._newRevNode(rev) self._populate = _populaterepo self._rootpopulated = False 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 fileData(self, index): """Returns the displayable file data at the given index""" repo = self._repoagent.rawRepo() if not index.isValid(): return filedata.createNullData(repo) f = index.internalPointer() e = f.parent while e and e.ctx is None: e = e.parent assert e, 'root entry must have ctx' wfile = hglib.fromunicode(f.path[len(e.path):].lstrip('/')) rpath = hglib.fromunicode(e.path) if f.subkind: # TODO: use subrepo ctxs and status resolved by this model return filedata.createSubrepoData(e.ctx, e.pctx, wfile, f.status, rpath, f.subkind) if f.isdir: return filedata.createDirData(e.ctx, e.pctx, wfile, rpath) return filedata.createFileData(e.ctx, e.pctx, wfile, f.status, rpath) def subrepoType(self, index): """Return the subrepo type the specified index""" if not index.isValid(): return e = index.internalPointer() return e.subkind def fileIcon(self, index): if not index.isValid(): return QIcon() e = index.internalPointer() k = (e.path, e.status, e.subkind) try: return self._iconcache[k] except KeyError: self._iconcache[k] = ic = self._makeFileIcon(e) return ic def _makeFileIcon(self, e): if e.subkind in _subrepoType2IcoMap: ic = qtlib.geticon(_subrepoType2IcoMap[e.subkind]) # use fine-tuned status overlay if any n = _subrepoStatus2IcoMap.get(e.status) if n: return qtlib.getoverlaidicon(ic, qtlib.geticon(n)) ic = qtlib.getoverlaidicon(ic, qtlib.geticon('thg-subrepo')) elif e.isdir: ic = self._fileiconprovider.icon(QFileIconProvider.Folder) else: # do not use fileiconprovier.icon(fileinfo), which may return icon # with shell (i.e. status of working directory) overlay. # default file icon looks ugly with status overlay on Windows ic = qtlib.geticon('text-x-generic') if not e.status: return ic st = status.statusTypes[e.status] if st.icon: icOverlay = qtlib.geticon(st.icon) 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() # TODO: 'S' should not be a status if e.subkind: return 'S' return e.status # TODO: this should be merged to fileStatus() def subrepoStatus(self, index): """Return the change status of the specified subrepo""" if not index.isValid(): return e = index.internalPointer() if not e.subkind: return return e.status def isDir(self, index): if not index.isValid(): return True # root entry must be a directory e = index.internalPointer() return e.isdir def mimeData(self, indexes): files = [self.filePath(i) for i in indexes if i.isValid()] ctx = self._rootentry.ctx if ctx.rev() is not None: repo = self._repoagent.rawRepo() lfiles = map(hglib.fromunicode, files) lbase, _fns = visdiff.snapshot(repo, lfiles, ctx) base = hglib.tounicode(lbase) else: # working copy base = self._repoagent.rootPath() m = QMimeData() m.setUrls([QUrl.fromLocalFile(os.path.join(base, e)) for e in files]) return m def mimeTypes(self): return ['text/uri-list'] def flags(self, index): f = super(ManifestModel, self).flags(index) if not index.isValid(): return f if not (self.isDir(index) or self.fileStatus(index) == 'R' or self._populate is _populatepatch): f |= Qt.ItemIsDragEnabled return f def index(self, row, column, parent=QModelIndex()): if row < 0 or self.rowCount(parent) <= row or column != 0: return QModelIndex() return self.createIndex(row, column, self._parententry(parent).at(row)) 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() try: e = self._nodeop.findpath(self._rootentry, unicode(path)) 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 def rev(self, parent=QModelIndex()): """Revision number of the current changectx""" e = self._parententry(parent) if e.ctx is None or not _isreporev(e.ctx.rev()): return -1 return e.ctx.rev() def baseRev(self, parent=QModelIndex()): """Revision of the base changectx where status is calculated from""" e = self._parententry(parent) if e.pctx is None or not _isreporev(e.pctx.rev()): return -1 return e.pctx.rev() def setRev(self, rev, prev=FirstParent): """Change to the specified repository revision; None for working-dir""" roote = self._rootentry newroote = self._newRevNode(rev, prev) if (_samectx(newroote.ctx, roote.ctx) and _samectx(newroote.pctx, roote.pctx)): return self._populate = _populaterepo self._repopulateNodes(newroote=newroote) if self._rootpopulated: self.revLoaded.emit(self.rev()) def setRawContext(self, ctx): """Change to the specified changectx in place of repository revision""" if _samectx(self._rootentry.ctx, ctx): return if _isreporev(ctx.rev()): repo = self._repoagent.rawRepo() try: if ctx == repo[ctx.rev()]: return self.setRev(ctx.rev()) except error.RepoLookupError: pass newroote = _Entry() newroote.ctx = ctx self._populate = _populatepatch self._repopulateNodes(newroote=newroote) if self._rootpopulated: self.revLoaded.emit(self.rev()) def nameFilter(self): """Return the current name filter""" return self._namefilter @pyqtSlot(str) def setNameFilter(self, pattern): """Filter file name by partial match of glob pattern""" pattern = unicode(pattern) if self._namefilter == pattern: return self._namefilter = pattern self._repopulateNodes() def statusFilter(self): """Return the current status filter""" return self._statusfilter # TODO: split or remove 'S' which causes several design flaws @pyqtSlot(str) def setStatusFilter(self, status): """Filter file tree by change status 'MARSC'""" status = str(status) assert all(c in 'MARSC' for c in status) if self._statusfilter == status: return # for performance reason self._statusfilter = status self._repopulateNodes() def isChangedFilesOnly(self): """Whether or not to filter by ctx.files, i.e. to exclude files not changed in the current revision. If this filter is enabled, 'C' (clean) files are not listed. For merge changeset, 'M' (modified) files in one side are also excluded. """ return self._changedfilesonly def setChangedFilesOnly(self, changedonly): if self._changedfilesonly == bool(changedonly): return self._changedfilesonly = bool(changedonly) self._repopulateNodes() def isFlat(self): """Whether all entries are listed in the same level or per directory""" return self._nodeop is _listnodeop def setFlat(self, flat): if self.isFlat() == bool(flat): return # self._nodeop must be changed after layoutAboutToBeChanged; otherwise # client code may obtain invalid indexes in its slot self._repopulateNodes(newnodeop=_nodeopmap[bool(flat)]) def canFetchMore(self, parent): if parent.isValid(): return False return not self._rootpopulated def fetchMore(self, parent): if parent.isValid() or self._rootpopulated: return assert len(self._rootentry) == 0 newroote = self._rootentry.copyskel() self._populateNodes(newroote) last = len(newroote) - 1 if last >= 0: self.beginInsertRows(parent, 0, last) self._rootentry = newroote self._rootpopulated = True if last >= 0: self.endInsertRows() self.revLoaded.emit(self.rev()) def _repopulateNodes(self, newnodeop=None, newroote=None): """Recreate populated nodes if any""" if not self._rootpopulated: # no stale nodes if newnodeop: self._nodeop = newnodeop if newroote: self._rootentry = newroote return self.layoutAboutToBeChanged.emit() try: oldindexmap = [(i, self.filePath(i)) for i in self.persistentIndexList()] if newnodeop: self._nodeop = newnodeop if not newroote: newroote = self._rootentry.copyskel() self._populateNodes(newroote) self._rootentry = newroote for oi, path in oldindexmap: self.changePersistentIndex(oi, self.indexFromPath(path)) finally: self.layoutChanged.emit() def _newRevNode(self, rev, prev=FirstParent): """Create empty root node for the specified revision""" if not _isreporev(rev): raise ValueError('unacceptable revision number: %r' % rev) if not _isreporev(prev): raise ValueError('unacceptable parent revision number: %r' % prev) repo = self._repoagent.rawRepo() roote = _Entry() roote.ctx = repo[rev] if prev == ManifestModel.FirstParent: roote.pctx = roote.ctx.p1() elif prev == ManifestModel.SecondParent: roote.pctx = roote.ctx.p2() else: roote.pctx = repo[prev] return roote def _populateNodes(self, roote): repo = self._repoagent.rawRepo() lpat = hglib.fromunicode(self._namefilter) match = _makematcher(repo, roote.ctx, lpat, self._changedfilesonly) self._populate(roote, repo, self._nodeop, self._statusfilter, match) roote.sort() class _Entry(object): """Each file or directory""" __slots__ = ('_name', '_parent', 'status', 'ctx', 'pctx', 'subkind', '_child', '_nameindex') def __init__(self, name='', parent=None): self._name = name self._parent = parent self.status = None self.ctx = None self.pctx = None self.subkind = None self._child = {} self._nameindex = [] def copyskel(self): """Create unpopulated copy of this entry""" e = self.__class__() e.status = self.status e.ctx = self.ctx e.pctx = self.pctx e.subkind = self.subkind return e @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 isdir(self): return bool(self.subkind or self._child) def __len__(self): return len(self._child) def __nonzero__(self): # leaf node should not be False because of len(node) == 0 return True def __getitem__(self, name): return self._child[name] def makechild(self, name): if name not in self._child: self._nameindex.append(name) self._child[name] = e = self.__class__(name, parent=self) return e def putchild(self, name, e): assert not e.name and not e.parent e._name = name e._parent = self if name not in self._child: self._nameindex.append(name) self._child[name] = e 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].isdir, os.path.normcase(s)), reverse=reverse) def _isreporev(rev): # patchctx.rev() returns str, which isn't a valid repository revision return rev is None or isinstance(rev, int) def _samectx(ctx1, ctx2): # no fast way to detect changes in uncommitted ctx, just assumes different if ctx1.rev() is None or not _isreporev(ctx1.rev()): return False # compare hash in case it was stripped and recreated (e.g. by qrefresh) return ctx1 == ctx2 and ctx1.node() == ctx2.node() # TODO: visual feedback to denote query type and error as in repofilter def _makematcher(repo, ctx, pat, changedonly): cwd = '' # always relative to repo root patterns = [] if pat and ':' not in pat and '*' not in pat: # mimic case-insensitive partial string match patterns.append('relre:(?i)' + re.escape(pat)) elif pat: patterns.append(pat) include = [] if changedonly: include.extend('path:%s' % p for p in ctx.files()) if not include: # no match return matchmod.exact(repo.root, cwd, []) try: return matchmod.match(repo.root, cwd, patterns, include=include, default='relglob', auditor=repo.auditor, ctx=ctx) except (error.Abort, error.ParseError): # no match return matchmod.exact(repo.root, cwd, []) class _listnodeop(object): subreporecursive = False @staticmethod def findpath(e, path): return e[path] @staticmethod def makepath(e, path): return e.makechild(path) @staticmethod def putpath(e, path, c): e.putchild(path, c) class _treenodeop(object): subreporecursive = True @staticmethod def findpath(e, path): for p in path.split('/'): e = e[p] return e @staticmethod def makepath(e, path): for p in path.split('/'): if p not in e: e.makechild(p) e = e[p] return e @staticmethod def putpath(e, path, c): rp = path.rfind('/') if rp >= 0: e = _treenodeop.makepath(e, path[:rp]) e.putchild(path[rp + 1:], c) _nodeopmap = { False: _treenodeop, True: _listnodeop, } def _populaterepo(roote, repo, nodeop, statusfilter, match): if 'S' in statusfilter: _populatesubrepos(roote, repo, nodeop, statusfilter, match) ctx = roote.ctx pctx = roote.pctx repo.lfstatus = True try: stat = repo.status(pctx, ctx, match, clean='C' in statusfilter) finally: repo.lfstatus = False for st, files in zip('MAR!?IC', stat): if st not in statusfilter: continue for path in files: e = nodeop.makepath(roote, hglib.tounicode(path)) e.status = st def _comparesubstate(state1, state2): if state1 == state2: return 'C' elif state1 == subrepo.nullstate: return 'A' elif state2 == subrepo.nullstate: return 'R' else: return 'M' def _populatesubrepos(roote, repo, nodeop, statusfilter, match): ctx = roote.ctx pctx = roote.pctx subpaths = set(pctx.substate) subpaths.update(ctx.substate) for path in subpaths: substate = ctx.substate.get(path, subrepo.nullstate) psubstate = pctx.substate.get(path, subrepo.nullstate) e = _Entry() e.status = _comparesubstate(psubstate, substate) if e.status == 'R': # denotes the original subrepo has been removed e.subkind = psubstate[2] else: e.subkind = substate[2] # do not call ctx.sub() unnecessarily, which may raise Abort or OSError # if git or svn executable not found if (nodeop.subreporecursive and e.subkind == 'hg' and e.status != 'R' and os.path.isdir(repo.wjoin(path))): smatch = matchmod.subdirmatcher(path, match) try: srepo = ctx.sub(path)._repo e.ctx = srepo[substate[1]] e.pctx = srepo[psubstate[1] or 'null'] _populaterepo(e, srepo, nodeop, statusfilter, smatch) except (error.RepoError, EnvironmentError): pass # subrepo is filtered out only if the node and its children do not # match the specified condition at all if len(e) > 0 or (e.status in statusfilter and match(path)): nodeop.putpath(roote, hglib.tounicode(path), e) def _populatepatch(roote, repo, nodeop, statusfilter, match): ctx = roote.ctx stat = ctx.changesToParent(0) for st, files in zip('MAR', stat): if st not in statusfilter: continue for path in files: if not match(path): continue e = nodeop.makepath(roote, hglib.tounicode(path)) e.status = st class ManifestCompleter(QCompleter): """QCompleter for ManifestModel""" def splitPath(self, path): """ >>> c = ManifestCompleter() >>> c.splitPath(u'foo/bar') [u'foo', u'bar'] trailing slash appends extra '', so that QCompleter can descend to next level: >>> c.splitPath(u'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-4.5.2/tortoisehg/hgqt/blockmatcher.py0000644000175000017500000003062613150123225022666 0ustar sborhosborho00000000000000# 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 """ from __future__ import absolute_import from .qtcore import ( Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QColor, QFrame, QHBoxLayout, QPainter, QPainterPath, QScrollBar, QSizePolicy, QWidget, ) 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) sy = float(h) / (self._maximum - self._minimum + self._pagestep) for typ, alo, ahi in self._blocks: color = self.blockTypes[typ] p.setPen(color) # make sure the height is at least 1px p.setBrush(color) p.drawRect(1, alo * sy, w - 1, (ahi - alo) * sy) p.setPen(self._vrectbordercolor) p.setBrush(self._vrectcolor) p.drawRect(0, self._value * sy, w, self._pagestep * sy) 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, str) valueChanged = pyqtSignal(int, str) pageStepChanged = pyqtSignal(int, str) 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)) def createTestWidget(ui, parent=None): f = QFrame(parent) 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) ui.status('sb1=%d %d %d\n' % (sb1.minimum(), sb1.maximum(), sb1.pageStep())) ui.status('sb2=%d %d %d\n' % (sb2.minimum(), sb2.maximum(), sb2.pageStep())) return f tortoisehg-4.5.2/tortoisehg/hgqt/graft.py0000644000175000017500000002655413153775104021354 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import import os from .qtcore import ( QSettings, QTimer, Qt, pyqtSlot, ) from .qtgui import ( QCheckBox, QDialog, QDialogButtonBox, QGroupBox, QMessageBox, QVBoxLayout, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, csinfo, cslist, qtlib, resolve, thgrepo, wctxcleaner, ) BB = QDialogButtonBox class GraftDialog(QDialog): def __init__(self, repoagent, parent, **opts): super(GraftDialog, self).__init__(parent) self.setWindowIcon(qtlib.geticon('hg-transplant')) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() self._graftstatefile = self.repo.vfs.join('graftstate') 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) self.srcb = srcb = QGroupBox() srcb.setLayout(QVBoxLayout()) srcb.layout().setContentsMargins(*(2,)*4) self.cslist = cslist.ChangesetList(self.repo) self._updateSource(0) srcb.layout().addWidget(self.cslist) self.layout().addWidget(srcb) destrev = self.repo['.'].rev() style = csinfo.panelstyle(selectable=True) 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._optchks = {} for name, text in [ ('currentuser', _('Use my user name instead of graft ' 'committer user name')), ('currentdate', _('Use current date')), ('log', _('Append graft info to log message')), ('autoresolve', _('Automatically resolve merge conflicts ' 'where possible'))]: self._optchks[name] = w = QCheckBox(text) self.layout().addWidget(w) self._cmdlog = cmdui.LogWidget(self) self._cmdlog.hide() self.layout().addWidget(self._cmdlog, 2) self._stbar = cmdui.ThgStatusBar(self) self._stbar.setSizeGripEnabled(False) self._stbar.linkActivated.connect(self.linkActivated) self.layout().addWidget(self._stbar) 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._stbar.showMessage(_('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') % repoagent.displayName()) self._readSettings() @property def repo(self): return self._repoagent.rawRepo() def _readSettings(self): ui = self.repo.ui qs = QSettings() qs.beginGroup('graft') for n, w in self._optchks.iteritems(): if n == 'autoresolve': w.setChecked(ui.configbool('tortoisehg', n, qtlib.readBool(qs, n, True))) else: w.setChecked(qtlib.readBool(qs, n)) qs.endGroup() def _writeSettings(self): qs = QSettings() qs.beginGroup('graft') for n, w in self._optchks.iteritems(): qs.setValue(n, w.isChecked()) qs.endGroup() 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.cslist.update(self.sourcelist[idx:]) @pyqtSlot(bool) def _onCheckFinished(self, clean): if not clean: self.graftbtn.setEnabled(False) txt = _('Before graft, you must ' 'commit, ' 'shelve to patch, ' 'or discard changes.') else: self.graftbtn.setEnabled(True) txt = _('You may continue or start the graft') self._stbar.showMessage(txt) def graft(self): self.graftbtn.setEnabled(False) self.cancelbtn.setVisible(False) opts = dict((n, w.isChecked()) for n, w in self._optchks.iteritems()) itool = opts.pop('autoresolve') and 'merge' or 'fail' opts['config'] = 'ui.merge=internal:%s' % itool if os.path.exists(self._graftstatefile): opts['continue'] = True args = [] else: args = [hglib.tounicode(str(s)) for s in self.sourcelist] cmdline = hglib.buildcmdargs('graft', *args, **opts) sess = self._runCommand(cmdline) sess.commandFinished.connect(self._graftFinished) def abort(self): self.abortbtn.setDisabled(True) if os.path.exists(self._graftstatefile): # Remove the existing graftstate file! os.remove(self._graftstatefile) cmdline = hglib.buildcmdargs('update', clean=True, rev='p1()') sess = self._runCommand(cmdline) sess.commandFinished.connect(self._abortFinished) def graftstate(self): graftstatefile = self.repo.vfs.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 _runCommand(self, cmdline): assert self._cmdsession.isFinished() self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) sess.commandFinished.connect(self._stbar.clearProgress) sess.outputReceived.connect(self._cmdlog.appendLog) sess.progressReceived.connect(self._stbar.setProgress) cmdui.updateStatusMessage(self._stbar, sess) return sess @pyqtSlot(int) def _graftFinished(self, ret): if self.checkResolve() is False: msg = _('Graft is complete') if ret == 255: msg = _('Graft failed') self._cmdlog.show() # contains hint else: self._updateSource(len(self.sourcelist) - 1) self._stbar.showMessage(msg) self._makeCloseButton() @pyqtSlot() def _abortFinished(self): if self.checkResolve() is False: self._stbar.showMessage(_('Graft aborted')) self._makeCloseButton() def _makeCloseButton(self): 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._stbar.showMessage(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() def done(self, r): self._writeSettings() super(GraftDialog, self).done(r) tortoisehg-4.5.2/tortoisehg/hgqt/graph.py0000644000175000017500000010063613251112733021334 0ustar sborhosborho00000000000000# graph.py - helper functions and classes to ease hg revision graph building # # 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. """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. """ r""" How each edge color is determined ================================= Legends ------- o, 0, 1, 2, ..., 9 visible revision x hidden revision `|a`, `a|` ("a" can be a-z) graph edge. edges with same alphabet have same color Rules ----- A. Edges on the same first-ancestors-line have same color .. code:: o |a o |a o B. Edges on branched-merged line have different color from base line .. code:: o o a| |\b o | o a|\b a| |\c | o | | \ o |b | | o a| o o |b | |/b a| | /c o | |/ a| | o o o |b a| o |/b o a| o C. Merged edge has same color as merged-from line .. code:: 9 |\ all merged lines(1-3, 4-6, 7-9) and right line(0-1-4-7-9) have 8 | same color | 7 6 | |\| 5 | | 4 3 | |\| 2 | | 1 |/ 0 D. Edges on the same first-ancestors-line have same color even if separated by revset .. code:: 4 a| Sometimes graph is separated into several parts by revset filter. 3 : All edges on the same first-ancestors-line have same color, x even if they are separated by filter. : 1 a| 0 E. Grafted line has different color from source, destination, and other grafted lines .. code:: 5 |\ a| \ 1-4 and 2-5 are grafted line 4 \ a|\c :d 3 : : | : 2 a| :/b | 1 |/b 0 Family line implementation ========================== Terms ----- Edge line which connect two revisions directly Path unbranched line which connect two revisions directly or indirectly. (Intermediate revisions can exist on the path) Parent line Edge between revision and its direct parent Family line Extension edge to complete revision depencency on the filtered graph. Next visible ancestor(s) Next visible ancestors of rev.X means the ancestor revisions that are neighboring with rev.X when ignoring hidden revisions. Description ----------- In the filtered dag with family line support, we must show at least one path between any visible revision and any ancestor of it. Examples -------- Legends ~~~~~~~ o, 0, 1, 2, ..., 9 visible revision x hidden revision `|<` family line Simple cases ~~~~~~~~~~~~ .. code:: ALL FILTERED ALL FILTERED ALL FILTERED 3 3 4 4 4 4 | |< |\ |\< |\ |\ x -> |< | x | |< | 3 | 3 | |< | | -> | |< | | -> | |< 1 1 | 2 | 2 | x | |< |/ |/ |/ |/< 1 1 1 1 Advanced cases ~~~~~~~~~~~~~~ ..code:: ALL FILTERED 3 3 |\ | No family line is drawn at 3-1 | x | because there is already parent line. |/ | 1 1 ALL FILTERED 6 6 |\ | 1 and 3 are next visible ancestors of 6, 5 | 5 but no family lines are drawn at 6-3 and 6-1, | x | because 6-3 and 6-1 path already exist (6-5-3-1) |/| -> | 3 | 3 | x | |/ | 1 1 ALL FILTERED 5 5 |\ |< Both 1 and 2 are next visible ancestors of 5, x | |< but no family line is drawn at 5-1 because 5-2 edge | x -> |< completes 5-1 path at the same time. 2 | 2 |/ | 1 1 Given such cases, we can determine family line location as below: If Rev-X and Rev-Y (X > Y) meets these all conditions, family line will be drawn between X and Y. 1. X and Y are both visible (not hidden) 2. Y is ancestor of X 3. Revisions in DAG between X and Y are all hidden 4. Y is *NOT* ancestor of visible parents of X 5. Y is *NOT* ancestor of any other lower-end revisions of family line from X """ import time import os import collections from mercurial import revset as revsetmod from mercurial import error, hg, node, phases from tortoisehg.util import obsoleteutil LINE_TYPE_PARENT = 0 LINE_TYPE_FAMILY = 1 LINE_TYPE_GRAFT = 2 LINE_TYPE_OBSOLETE = 3 NODE_SHAPE_REVISION = 0 NODE_SHAPE_CLOSEDBRANCH = 1 NODE_SHAPE_APPLIEDPATCH = 2 NODE_SHAPE_UNAPPLIEDPATCH = 3 NODE_SHAPE_REVISION_DRAFT = 4 NODE_SHAPE_REVISION_SECRET = 5 # TODO: Remove these two when we adopt GTK author color scheme COLORS = [ '#0000ff', # blue '#006400', # dark green '#008000', # green '#00008b', # dark blue '#800080', # purple '#1e90ff', # dodger blue '#808000', # dark yellow '#ff00ff', # magenta '#8b008b', # dark magenta '#008b8b', # dark cyan ] def hashcolor(data, modulo=None): """function to reliably map a string to a color index The algorithm used is very basic and can be improved if needed. """ if modulo is None: modulo = len(COLORS) idx = sum([ord(c) for c in data]) idx %= modulo return idx class StandardDag(object): """Generate DAG for grapher Public fields: repo The repository start_rev Tip-most revision of range to graph This can be None, which means workingtree stop_rev 0-most revision of range to graph branch If set, then only revisions in this branch only iterated. allparents If set in addition to branch, then cset outside the branch that are ancestors to some cset inside the branch is also iterated showgraftsource If set, return graft relations additionally visiblerev The function to determine revision visiblity, which accepts one argument(revno) and return bool value (True if visible) walk() iterates visible nodes with this form (ctx is changectx or filectx): `(ctx, [(parent ctx, line type, p1 or not), ...])` """ def __init__(self, repo, start_rev, stop_rev, branch, allparents, showgraftsource, visiblerev): assert start_rev is None or start_rev >= stop_rev self.repo = repo self.start_rev = start_rev self.stop_rev = stop_rev self.branch = branch self.allparents = allparents self.showgraftsource = showgraftsource self.visiblerev = visiblerev if self.allparents or not branch: def visiblectx(ctx): return bool(ctx) else: def visiblectx(ctx): return ctx and ctx.branch() == branch self.visiblectx = visiblectx def _iter_revs(self, repo, visiblerev): stop_rev = self.stop_rev curr_rev = self.start_rev if curr_rev is None: if visiblerev(curr_rev): yield repo[curr_rev] curr_rev = len(repo) - 1 # jump in the branch grouping graph experiment if the user subscribed revs = revsetmod.spanset(repo, curr_rev, stop_rev - 1) if revs and repo.ui.configbool('experimental', 'graph-group-branches', False): start, stop = revs.first(), revs.last() revset = 'sort(%d:%d, "topo"' args = [start, stop] firstbranchrevset = repo.ui.config( 'experimental', 'graph-group-branches.firstbranch', '') if firstbranchrevset: revset += ', topo.firstbranch=%s' args.append(firstbranchrevset) revset += ')' revs = repo.revs(revset, *args) for curr_rev in revs: if visiblerev(curr_rev): yield repo[curr_rev] def _append_graft_source(self, ctx, parents): src_rev_str = ctx.extra().get('source') if src_rev_str is not None and src_rev_str in self.repo: src = self.repo[src_rev_str] src_rev = src.rev() if self.stop_rev <= src_rev < ctx.rev() and \ self.visiblerev(src_rev) and self.visiblectx(src): parents.append((src, LINE_TYPE_GRAFT, False)) for octx in obsoleteutil.first_known_precursors(ctx): src_rev = octx.rev() if self.stop_rev <= src_rev < ctx.rev() and \ self.visiblerev(src_rev) and self.visiblectx(octx): parents.append((octx, LINE_TYPE_OBSOLETE, False)) def walk(self): repo = self.repo branch = self.branch showgraftsource = self.showgraftsource visiblerev = self.visiblerev visiblectx = self.visiblectx upcomingparents = set() for ctx in self._iter_revs(repo, visiblerev): if ctx.rev() not in upcomingparents: if branch and ctx.branch() != branch: continue else: upcomingparents.remove(ctx.rev()) parents = [(p, LINE_TYPE_PARENT, i == 0) for i, p in enumerate(filter(visiblectx, ctx.parents())) if visiblerev(p.rev())] if showgraftsource: self._append_graft_source(ctx, parents) upcomingparents.update([p[0].rev() for p in parents]) yield ctx, parents class _FamilyLineRev(object): r"""Revision information for building family line relations Public fields: rev Revision number. Can be None (means workingdir) visible True if self should be shown destinations List of parent/family line edge destinations. Each elements are tuple: revno revision number of edge destination edge linetype LINE_TYPE_PARENT or LINE_TYPE_FAMILY is_p1 True if revno is in ancestors(p1(self.rev)) next_descendants dictionary: key _FamilyLineRev which can be upper-end of family line edge to self.rev value True if self.rev is in ancestors(p1(key.rev)) excluded_descendants frozenset of _FamilyLineRev. Revisions which are excluded from next_descendants. family line is *NOT* drawn between self and these revisions. pending Number of unclosed edges of which upper-end is self.rev Initial value is number of hidden parents(set by proceed()), and incremented or decremented with proceeding DAG scan. It will become 0 when all NVAs of self are determined. This is illustration of relations between instances : +--------------------------------------+ o | upper visible revision | | +--------------------------------------+ x next_descendants ^ | destinations ^ |\ | v (FAMILY) | | | +--------------+ | @ | | self | | next_descendants | | +--------------+ | excluded_descendants | x excluded_descendants ^ | destinations | | | (*1) | v (PARENT) | |/ +--------------------------------------+ o | lower visible revision | : +--------------------------------------+ (*1) because here is parent line, not family line """ __slots__ = ["rev", "visible", "next_descendants", "excluded_descendants", "pending", "destinations"] def __init__(self, rev, visible): self.rev = rev self.visible = visible self.next_descendants = {} self.excluded_descendants = set() self.pending = 0 self.destinations = [] def proceed(self, parents): next_descendants = self.next_descendants excluded_descendants = self.excluded_descendants excluded_descendants.difference_update([r for r in excluded_descendants if not r.pending]) # decrement `pending` of each next_descendants regardless of # self.visible once. # (it will be reincremented if self is hidden and self has parents) for nd in next_descendants: nd.pending -= 1 assert nd.pending >= 0 if excluded_descendants: next_descendants = dict(kv for kv in next_descendants.items() if kv[0] not in excluded_descendants) if self.visible: for nd, is_p1 in next_descendants.iteritems(): nd.destinations.append((self.rev, LINE_TYPE_FAMILY, is_p1)) # `next_descendants` are also excluded from next_descendants # of parents because of definition #4 parent_ed = excluded_descendants.union(next_descendants) for i, p in enumerate(parents): p.add_excluded_descendants(parent_ed) if p.visible: self.destinations.append((p.rev, LINE_TYPE_PARENT, i == 0)) p.add_excluded_descendants([self]) else: p.add_next_descendants({self: i == 0}) else: # just pass to parents for p in parents: p.add_next_descendants(next_descendants) p.add_excluded_descendants(excluded_descendants) # these are no longer needed self.next_descendants = self.excluded_descendants = None def add_next_descendants(self, descendants): for d, is_p1 in descendants.iteritems(): if d in self.next_descendants: self.next_descendants[d] |= is_p1 else: d.pending += 1 self.next_descendants[d] = is_p1 def add_excluded_descendants(self, descendants): self.excluded_descendants.update(descendants) def __hash__(self): return hash(self.rev) def __eq__(self, other): return isinstance(other, _FamilyLineRev) and self.rev == other.rev def __ne__(self, other): return not self.__eq__(other) def __repr__(self): if self.rev is None: return "_FamilyLineRev(+)" else: return "_FamilyLineRev(%d)" % self.rev class FamilyLineDag(StandardDag): """Generate filtered DAG with family lines for grapher""" def walk(self): repo = self.repo stop_rev = self.stop_rev showgraftsource = self.showgraftsource upcomingrevs = {} visiblerev = self.visiblerev visiblectx = self.visiblectx def get_or_create_rev(ctx): rev = ctx.rev() ret = upcomingrevs.get(rev) if not ret: ret = upcomingrevs[rev] = \ _FamilyLineRev(rev, visiblerev(rev) and visiblectx(ctx)) return ret queue = collections.deque() for ctx in self._iter_revs(repo, lambda rev: True): rev = upcomingrevs.pop(ctx.rev(), None) if not rev: if not visiblerev(ctx.rev()) or not visiblectx(ctx): continue rev = _FamilyLineRev(ctx.rev(), True) parents = [get_or_create_rev(p) for p in ctx.parents() if p.rev() >= stop_rev] rev.proceed(parents) if rev.visible: queue.append(rev) # yield after rev.pending becomes 0 while queue and not queue[0].pending: r = queue.popleft() # order by p1 -> p2, small rev -> large rev destinations = sorted(r.destinations, key=lambda e: (not e[2], e[0])) parents = [(repo[pno], linktype, is_p1) for (pno, linktype, is_p1) in destinations] rctx = repo[r.rev] if showgraftsource: self._append_graft_source(rctx, parents) yield rctx, parents assert not queue def revision_grapher(repo, opts): """incremental revision grapher param repo The repository opt revset set of revisions to graph. 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 opt showfamilyline If set in addition to revset, then family line will be shown between descendants and ancestors This generator function walks through the revision range in descending order. When revset is specified, range is from max(revset) to min(revset), otherwise from working tree(pseudo revision) to rev0. 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), edge) defining the edges between the current row and the next row - parent revisions of current revision """ revset = opts.get('revset') if revset: start_rev = max(revset) stop_rev = min(revset) visiblerev = lambda rev: rev in revset else: start_rev = None stop_rev = 0 visiblerev = lambda rev: True if revset and opts.get('showfamilyline'): cls = FamilyLineDag else: cls = StandardDag dag = cls(repo, start_rev, stop_rev, opts.get('branch'), opts.get('allparents'), opts.get('showgraftsource'), visiblerev) return _iter_graphnodes(dag, GraphNode.fromchangectx) def _iter_graphnodes(dag, nodefactory): revs = [] activeedges = [] # order is not important rev_color = RevColorPalette() for ctx, parents in dag.walk(): curr_rev = ctx.rev() # Compute revs and next_revs. if curr_rev not in revs: # New head. revs.append(curr_rev) rev_index = revs.index(curr_rev) next_revs = revs[:] activeedges = [e for e in activeedges if e.endrev < curr_rev] # Add parents to next_revs. parents_to_add = [] for pctx, link_type, is_p1 in parents: parent = pctx.rev() if parent not in next_revs: # Because the parents originate from multiple sources, it is # theoretically possible that several point to the same # revision. Only take the first of this (which is graftsource # because it is added before). if parent in parents_to_add: continue parents_to_add.append(parent) if is_p1: color = rev_color[ctx] elif link_type in (LINE_TYPE_GRAFT, LINE_TYPE_OBSOLETE): color = rev_color.nextcolor() else: color = rev_color[pctx] activeedges.append(GraphEdge(curr_rev, parent, color, link_type)) next_revs[rev_index:rev_index + 1] = parents_to_add lines = [] for e in activeedges: if e.startrev == curr_rev: r = e.startrev else: r = e.endrev p = (revs.index(r), next_revs.index(e.endrev)) lines.append((p, e)) yield nodefactory(dag.repo, ctx, rev_index, lines) revs = next_revs def filelog_grapher(repo, path): ''' Graph the ancestry of a single file (log). Deletions show up as breaks in the graph. ''' if hasattr(repo, 'shallowmatch') and repo.shallowmatch(path): filedag = ShallowFileDag else: filedag = FileDag dag = filedag(repo, path) return _iter_graphnodes(dag, GraphNode.fromfilectx) class FileDag(object): def __init__(self, repo, path): self.repo = repo self.path = path def _getflogheads(self): flog = self.repo.file(self.path) return flog.heads() def walk(self): flogheads = self._getflogheads() heads = sorted(self.repo.filectx(self.path, fileid=x).rev() for x in flogheads) or [-1] rev = heads.pop() _paths = {} while rev >= 0: revpath = _paths.pop(rev, self.path) # Add parents to next_revs fctx = self.repo.filectx(revpath, changeid=rev) for pfctx in fctx.parents(): _paths[pfctx.rev()] = pfctx.path() parents = [(pfctx, LINE_TYPE_PARENT, i == 0) for i, pfctx in enumerate(fctx.parents())] yield fctx, parents if _paths: rev = max(_paths) else: rev = -1 if heads and rev <= heads[-1]: rev = heads.pop() class ShallowFileDag(FileDag): """FileDag specialization for shallow (remotefilelog) repository""" def _getflogheads(self): repo = self.repo path = self.path try: dest = repo.ui.expandpath('default') peer = hg.peer(repo, {}, dest) except error.RepoError: peer = None flogheads = set() repoheads = set(repo.heads()) # Get filelog heads from server and filter local heads with peer heads if peer is not None and peer.capable('getflogheads'): repoheads -= set(peer.heads()) flogheads.update(set(peer.getflogheads(path))) # Get filelog heads from local repo heads. # This allows to get changes not yet pushed to the server. This is # also a fallback in case the server is not available. for head in repoheads: try: fctx = repo[head].filectx(path) fnode = node.hex(fctx.filenode()) if fnode not in flogheads: flogheads.add(fnode) # We filter out parents of added node. # A parent could already be in flogheads if still a # head on the server, but not anymore in local repository flogheads -= set([node.hex(pfctx.filenode()) for pfctx in fctx.parents()]) except error.ManifestLookupError: pass return flogheads class RevColorPalette(object): """Assign node and line colors for each revision""" def __init__(self): self._pendingheads = [] self._knowncolors = {} self._curcolor = -1 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 = ctx0.parents() def nextcolor(self): self._curcolor += 1 return self._curcolor def __getitem__(self, ctx): rev = ctx.rev() if rev not in self._knowncolors: self._fillpendingheads(rev) if rev not in self._knowncolors: color = self.nextcolor() self._knowncolors[rev] = color p_ctxs = ctx.parents() self._pendingheads.append((p_ctxs, color)) return self._knowncolors[rev] class GraphEdge(tuple): __slots__ = () def __new__(cls, startrev, endrev, color, linktype=LINE_TYPE_PARENT): return tuple.__new__(cls, (startrev, endrev, color, linktype)) @property def startrev(self): return self[0] # int or None (for working rev) @property def endrev(self): return self[1] # int @property def color(self): return self[2] # int @property def linktype(self): return self[3] # one of LINE_TYPE def __repr__(self): xs = (self.__class__.__name__,) + self return '%s(%r->%r, color=%r, linktype=%r)' % xs @property def importance(self): """Sort key of overlapped edges; highest one should be drawn last""" # prefer parent-child relation and younger (i.e. longer) edge return -self[3], -self[2] class GraphNode(object): """Graph node for all actual changesets, as well as the working copy Simple class to encapsulate a hg node in the revision graph. Does nothing but declaring attributes. """ __slots__ = ["bottomlines", "extra", "hidden", "obsolete", "rev", "shape", "toplines", "instabilities", "wdparent", "x"] @classmethod def fromchangectx(cls, repo, ctx, xposition, lines): if ctx.thgmqappliedpatch(): shape = NODE_SHAPE_APPLIEDPATCH elif ctx.closesbranch(): shape = NODE_SHAPE_CLOSEDBRANCH elif phases.draft == ctx.phase(): shape = NODE_SHAPE_REVISION_DRAFT elif phases.secret <= ctx.phase(): shape = NODE_SHAPE_REVISION_SECRET else: shape = NODE_SHAPE_REVISION wdparent = ctx.node() in repo.dirstate.parents() return cls(shape, ctx=ctx, xposition=xposition, lines=lines, wdparent=wdparent) @classmethod def fromfilectx(cls, repo, fctx, xposition, lines): ctx = repo.unfiltered()[fctx.rev()] # get changectx wrapped by thgrepo obj = cls.fromchangectx(repo, ctx, xposition, lines) obj.extra = [fctx.path()] return obj def __init__(self, shape, ctx, xposition, lines, wdparent=False, extra=None): self.rev = ctx.rev() self.hidden = ctx.hidden() self.obsolete = ctx.obsolete() self.instabilities = ctx.instabilities() self.shape = shape self.x = xposition self.bottomlines = lines self.toplines = [] # set from Graph.__getitem__ self.wdparent = wdparent self.extra = extra @property def faded(self): """Indicates whether the node should be faded in the UI""" return self.hidden or self.obsolete @property def cols(self): """Number of columns for the node""" return max([self.x] + [max(p) for p, _e in self.bottomlines]) + 1 class PatchGraphNode(object): """Node for un-applied patch queue items. This node is always displayed unfaded and only occupy one column. Furthermore, the revision is the name of the patch. """ def __init__(self, name): self.name = name self.shape = NODE_SHAPE_UNAPPLIEDPATCH self.rev = name # unapplied patch uses its name as rev self.wdparent = False self.toplines = [] self.bottomlines = [] self.instabilities = () self.x = 0 @property def faded(self): """Indicates whether the node should be faded in the UI""" return False @property def cols(self): """Number of columns for the node""" return 1 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. """ def __init__(self, repo, grapher): self.repo = repo self.grapher = grapher self.nodes = [] self.nodesdict = {} 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() nnodes = -1 # infinite elif nnodes is None: nnodes = 0 if rev is not None and self.nodes: gnode = self.nodes[-1] if isinstance(gnode.rev, int) and gnode.rev <= rev: rev = None # already reached rev if rev is None and nnodes == 0: return True for gnode in self.grapher: if self.nodes: gnode.toplines = self.nodes[-1].bottomlines self.nodes.append(gnode) self.nodesdict[gnode.rev] = gnode if rev is None: nnodes -= 1 elif isinstance(gnode.rev, int) and gnode.rev <= rev: rev = None # we reached rev, switching to nnode counter if rev is None and nnodes == 0: return True if usetimer: cursec = timer() if cursec < startsec or cursec > startsec + 0.1: return True self.grapher = None return False 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) try: return self.nodes.index(self.nodesdict[rev]) except KeyError: raise ValueError('rev %r not found' % rev) # # File graph method # def filename(self, rev): return self.nodesdict[rev].extra[0] class GraphWithMq(object): """Graph layouter that also shows un-applied mq changes""" def __init__(self, graph, patchnames): object.__init__(self) self.graph = graph self._patchnames = list(reversed(patchnames)) def isfilled(self): """Indicates whether the graph is done computing""" return self.graph.isfilled() def build_nodes(self, fillstep=None, rev=None): """Ensures that the graph layout is computed""" self.graph.build_nodes(fillstep, rev) def __len__(self): return len(self._patchnames) + len(self.graph) def __getitem__(self, row): if row < len(self._patchnames): return PatchGraphNode(self._patchnames[row]) return self.graph[row - len(self._patchnames)] def index(self, rev): """Get row number for specified revision""" if isinstance(rev, str): return self._patchnames.index(rev) i = self.graph.index(rev) return len(self._patchnames) + i tortoisehg-4.5.2/tortoisehg/hgqt/purge.py0000644000175000017500000002277413153775104021373 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import shutil import stat from .qtcore import ( QSettings, QThread, QTimer, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QCheckBox, QDialog, QDialogButtonBox, QVBoxLayout, qApp, ) from mercurial import ( hg, scmutil, ) from ..util import hglib from ..util.i18n import _, ngettext from . import ( cmdui, qtlib, ) class PurgeDialog(QDialog): progress = pyqtSignal(str, object, str, str, object) showMessage = pyqtSignal(str) def __init__(self, repoagent, parent=None): QDialog.__init__(self, parent) f = self.windowFlags() self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint) self._repoagent = repoagent layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setLayout(layout) toplayout = QVBoxLayout() toplayout.setContentsMargins(10, 10, 10, 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) self.setWindowTitle(_('%s - purge') % repoagent.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(qtlib.readByteArray(s, 'purge/geom')) 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.lfstatus = True stat = repo.status(ignored=True, unknown=True) repo.lfstatus = False trashcan = repo.vfs.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(str, object, str, str, object) showMessage = pyqtSignal(str) 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(hglib.loadui(), self.root) keephg = opts['keephg'] directories = [] failures = [] if opts['trash']: self.showMessage.emit(_('Deleting trash folder...')) trashcan = repo.vfs.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.lfstatus = True status = repo.status(match=match, ignored=opts['ignored'], unknown=opts['unknown'], clean=False) repo.lfstatus = False files = [] for k, i in [('unknown', 4), ('ignored', 5)]: if opts[k]: files.extend(status[i]) 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-4.5.2/tortoisehg/hgqt/qscilib.py0000644000175000017500000007725213153775104021700 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import re import weakref from .qsci import ( QSCINTILLA_VERSION, QsciLexerProperties, QsciScintilla, ) from .qtcore import ( QObject, QEvent, QFile, QIODevice, QRect, QSettings, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAction, QCheckBox, QDialog, QDialogButtonBox, QInputMethodEvent, QKeyEvent, QKeySequence, QLabel, QLineEdit, QMenu, QToolBar, QVBoxLayout, qApp, ) from ..util import hglib from ..util.i18n import _ from . import qtlib # 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 https://doc.qt.io/qt-4.8/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""" # QScintilla 2.8.4 still can't handle input method events properly. # For example, it fails to delete the last preedit text by ^H, and # editing position goes wrong. So we sticks to our version. if True: def __init__(self, parent=None): super(ScintillaCompat, self).__init__(parent) self._imsupport = _SciImSupport(self) def inputMethodQuery(self, query): if query == Qt.ImMicroFocus: # 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)) 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) # TextFormat is not supported event.accept() # 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 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(qtlib.readInt(qs, prefix + '/wrap')) self.setWhitespaceVisibility(qtlib.readInt(qs, prefix + '/whitespace')) self.setEolVisibility(qtlib.readBool(qs, prefix + '/eol')) # usetabs = -1, False, or True usetabs = qtlib.readInt(qs, prefix + '/usetabs') if usetabs != -1: usetabs = qtlib.readBool(qs, prefix + '/usetabs') self.setIndentationsUseTabs(usetabs) self.setDefaultEolMode() self.setAutoCompletionThreshold( qtlib.readInt(qs, prefix + '/autocomplete', -1)) @pyqtSlot(str, 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(str, 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 = action.data() self.setWrapMode(mode) @pyqtSlot(QAction) def _setWhitespaceVisibilityByMenu(self, action): mode = action.data() self.setWhitespaceVisibility(mode) @pyqtSlot(QAction) def _setEolVisibilityByMenu(self, action): visible = action.data() self.setEolVisibility(visible) @pyqtSlot(QAction) def _setEolModeByMenu(self, action): mode = action.data() self.setEolMode(mode) @pyqtSlot(QAction) def _setIndentationsUseTabsByMenu(self, action): mode = action.data() 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(str, bool, bool) """Emitted (pattern, icase, wrap) when search condition changed""" searchRequested = pyqtSignal(str, bool, bool, bool) """Emitted (pattern, icase, wrap, forward) when requested""" def __init__(self, parent=None): super(SearchToolBar, self).__init__(_('Search'), parent, objectName='search') self.setIconSize(qtlib.smallIconSize()) a = self.addAction(qtlib.geticon('window-close'), '') a.setShortcut(Qt.Key_Escape) a.setShortcutContext(Qt.WidgetWithChildrenShortcut) a.triggered.connect(self.hide) 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._prevact = self.addAction(qtlib.geticon('go-up'), _('Prev')) self._prevact.setShortcuts(QKeySequence.FindPrevious) self._nextact = self.addAction(qtlib.geticon('go-down'), _('Next')) self._nextact.setShortcuts(QKeySequence.FindNext) for a in [self._prevact, self._nextact]: a.setShortcutContext(Qt.WidgetWithChildrenShortcut) a.triggered.connect(self._emitSearchRequested) w = self.widgetForAction(a) w.setAutoRaise(False) # no flat button w.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self._le.textChanged.connect(self._updateSearchButtons) 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.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._prevact.trigger() return if event.delta() < 0: self._nextact.trigger() 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(qtlib.readBool(self._settings, 'icase', False)) self.setWrapAround(qtlib.readBool(self._settings, 'wrap', False)) @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 = self.sender() is not self._prevact self.searchRequested.emit(self.pattern(), self.caseInsensitive(), self.wrapAround(), forward) def editorActions(self): """List of actions that should be available in main editor widget""" return [self._prevact, self._nextact] @pyqtSlot() def _updateSearchButtons(self): enabled = bool(self._le.text()) for a in [self._prevact, self._nextact]: a.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(str) 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 any(event.matches(e) for e in self._keyseqs): return True return False def unbindConflictedKeys(sci): cmdset = sci.standardCommands() try: cmd = cmdset.boundTo(Qt.CTRL + Qt.Key_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 readFile(editor, filename, encoding=None): f = QFile(filename) if not f.open(QIODevice.ReadOnly): qtlib.WarningMsgBox(_('Unable to read file'), _('Could not open the specified file for reading.'), f.errorString(), parent=editor) return False try: earlybytes = f.read(4096) if '\0' in earlybytes: qtlib.WarningMsgBox(_('Unable to read file'), _('This appears to be a binary file.'), parent=editor) return False f.seek(0) data = str(f.readAll()) if f.error(): qtlib.WarningMsgBox(_('Unable to read file'), _('An error occurred while reading the file.'), f.errorString(), parent=editor) return False finally: f.close() if encoding: try: text = data.decode(encoding) except UnicodeDecodeError, inst: qtlib.WarningMsgBox(_('Text Translation Failure'), _('Could not translate the file content from ' 'native encoding.'), (_('Several characters would be lost.') + '\n\n' + hglib.tounicode(str(inst))), parent=editor) text = data.decode(encoding, 'replace') else: text = hglib.tounicode(data) editor.setText(text) editor.setDefaultEolMode() editor.setModified(False) return True def writeFile(editor, filename, encoding=None): text = editor.text() try: if encoding: data = unicode(text).encode(encoding) else: data = hglib.fromunicode(text) except UnicodeEncodeError, inst: qtlib.WarningMsgBox(_('Unable to write file'), _('Could not translate the file content to ' 'native encoding.'), hglib.tounicode(str(inst)), parent=editor) return False f = QFile(filename) if not f.open(QIODevice.WriteOnly): qtlib.WarningMsgBox(_('Unable to write file'), _('Could not open the specified file for writing.'), f.errorString(), parent=editor) return False try: if f.write(data) < 0: qtlib.WarningMsgBox(_('Unable to write file'), _('An error occurred while writing the file.'), f.errorString(), parent=editor) return False finally: f.close() return True 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) 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.addActions(searchbar.editorActions()) 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(qtlib.readByteArray(s, geomname)) if not readFile(editor, filename): return QDialog.Rejected ret = dialog.exec_() if ret != QDialog.Accepted: return ret if not writeFile(editor, filename): return QDialog.Rejected s.setValue(geomname, dialog.saveGeometry()) return ret tortoisehg-4.5.2/tortoisehg/hgqt/revset.py0000644000175000017500000003172513205035322021542 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import from .qsci import ( QsciAPIs, QsciLexerPython, QsciScintilla, ) from .qtcore import ( QSize, Qt, pyqtSignal, ) from .qtgui import ( QColor, QDialog, QGroupBox, QHBoxLayout, QKeySequence, QLabel, QListWidget, QShortcut, QSizePolicy, QVBoxLayout, ) from ..util.i18n import _ from . import ( cmdui, qtlib, ) # TODO: # 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): queryIssued = pyqtSignal(str) def __init__(self, repoagent, parent=None): QDialog.__init__(self, parent) self._repoagent = repoagent # Since the revset dialot belongs to a repository, we display # the repository name in the dialog title self.setWindowTitle(_('Revision Set Query') + ' - ' + repoagent.displayName()) self.setWindowFlags(Qt.Window) layout = QVBoxLayout() layout.setContentsMargins(*(4,)*4) self.setLayout(layout) logical = _logical ancestry = _ancestry repo = repoagent.rawRepo() 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) # same policy as status bar of QMainWindow self.stbar.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) self.stbar.lbl.setOpenExternalLinks(True) 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) def runQuery(self): self.queryIssued.emit(self.entry.text()) 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()) tortoisehg-4.5.2/tortoisehg/hgqt/hgignore.py0000644000175000017500000002676313153775104022055 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import re from .qtcore import ( QEvent, QSettings, QTimer, Qt, pyqtSignal, ) from .qtgui import ( QAbstractItemView, QComboBox, QDialog, QDialogButtonBox, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QMenu, QPushButton, QSplitter, QVBoxLayout, ) from mercurial import ( commands, error, match, util, ) from ..util import ( hglib, shlib, ) from ..util.i18n import _ from . import ( qscilib, qtlib, ) 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 self.pats = pats self.setWindowTitle(_('Ignore filter - %s') % repoagent.displayName()) self.setWindowIcon(qtlib.geticon('thg-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.setAutoDefault(False) add.clicked.connect(self.addEntry) hbox.addWidget(add, 0) # layer 2 repo = repoagent.rawRepo() 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.setAutoDefault(False) 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.button(BB.Close).setAutoDefault(False) 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(qtlib.readByteArray(s, 'hgignore/geom')) @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 self.lclunknowns = self.repo.status(unknown=True)[4] 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.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) item.setSelected(True) self.unknownlist.setCurrentItem(item) self.le.setText(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.vfs.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(hglib.loadui(), 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-4.5.2/tortoisehg/hgqt/qtapp.py0000644000175000017500000005337313242607601021370 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import gc import os import platform import signal import sys import traceback from .qtcore import ( PYQT_VERSION, PYQT_VERSION_STR, QByteArray, QIODevice, QLibraryInfo, QObject, QSettings, QSignalMapper, QSocketNotifier, QT_API, QT_VERSION, QT_VERSION_STR, QTimer, QTranslator, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QApplication, QFont, ) from .qtnetwork import ( QLocalServer, QLocalSocket, ) from mercurial import ( error, util, ) from ..util import ( hglib, i18n, version as thgversion, ) from ..util.i18n import _ from . import ( bugreport, qtlib, thgrepo, workbench, ) if os.name == 'nt' and getattr(sys, 'frozen', False): # load QtSvg4.dll and QtXml4.dll by .pyd, so that imageformats/qsvg4.dll # can find them without relying on unreliable PATH variable. The filenames # change for PyQt5 but the basic problem remains the same. _mod = __import__(QT_API, globals(), locals(), ['QtSvg', 'QtXml']) _mod.QtSvg.__name__, _mod.QtXml.__name__ # no demandimport if PYQT_VERSION < 0x40705 or QT_VERSION < 0x40600: sys.stderr.write('TortoiseHg requires at least Qt 4.6 and PyQt 4.7.5\n') sys.stderr.write('You have Qt %s and PyQt %s\n' % (QT_VERSION_STR, PYQT_VERSION_STR)) sys.exit(-1) if getattr(sys, 'frozen', False) and os.name == 'nt': # load icons and translations from . import icons_rc, translations_rc try: from thginithook import thginithook except ImportError: thginithook = None def _ugetuser(): return hglib.tounicode(util.getuser()) # {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 self._originthandler = signal.signal(signal.SIGINT, self._inthandler) self._initWakeup() def release(self): if not self._origexcepthook: return self._ui.debug('restoring excepthook\n') sys.excepthook = self._origexcepthook self._origexcepthook = None signal.signal(signal.SIGINT, self._originthandler) self._originthandler = None self._releaseWakeup() 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: 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_() else: dlg = bugreport.BugReport(opts, parent=parent) dlg.exec_() def _printexception(self): for args in self.errors: traceback.print_exception(*args) def _inthandler(self, signum, frame): # QTimer makes sure to not enter new event loop in signal handler, # which will be invoked at random location. Note that some windows # may show modal confirmation dialog in closeEvent(). QTimer.singleShot(0, self._mainapp.closeAllWindows) if os.name == 'posix': # Wake up Python interpreter via pipe so that SIGINT can be handled # immediately. (https://doc.qt.io/qt-4.8/unix-signals.html) def _initWakeup(self): import fcntl rfd, wfd = os.pipe() for fd in (rfd, wfd): flags = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) self._wakeupsn = QSocketNotifier(rfd, QSocketNotifier.Read, self) self._wakeupsn.activated.connect(self._handleWakeup) self._origwakeupfd = signal.set_wakeup_fd(wfd) def _releaseWakeup(self): self._wakeupsn.setEnabled(False) rfd = self._wakeupsn.socket() wfd = signal.set_wakeup_fd(self._origwakeupfd) self._origwakeupfd = -1 os.close(rfd) os.close(wfd) @pyqtSlot() def _handleWakeup(self): # here Python signal handler will be invoked self._wakeupsn.setEnabled(False) rfd = self._wakeupsn.socket() try: os.read(rfd, 1) except OSError, inst: self._ui.debug('failed to read wakeup fd: %s\n' % inst) self._wakeupsn.setEnabled(True) else: # On Windows, non-blocking anonymous pipe or socket is not available. # So run Python instruction at a regular interval. Because it wastes # CPU time, it is disabled if thg is known to be detached from tty. def _initWakeup(self): self._wakeuptimer = 0 if self._ui._isatty(self._ui.fin): self._wakeuptimer = self.startTimer(200) def _releaseWakeup(self): if self._wakeuptimer > 0: self.killTimer(self._wakeuptimer) self._wakeuptimer = 0 def timerEvent(self, event): # nop for instant SIGINT handling pass 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() + '-' + _ugetuser() 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) and os.name == 'nt': 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 https://doc.qt.io/qt-4.8/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._reporeleaser = None self._mainreporoot = None self._workbench = None def __call__(self, dlgfunc, ui, *args, **opts): if self._mainapp: self._opendialog(dlgfunc, args, opts) return QSettings.setDefaultFormat(QSettings.IniFormat) # fixes font placement on OSX 10.9 with QT <= 4.8.5 # see QTBUG-32789 (https://bugreports.qt-project.org/browse/QTBUG-32789) if sys.platform == 'darwin' and QT_VERSION <= 0x040805: version = platform.mac_ver()[0] version = '.'.join(version.split('.')[:2]) if version == '10.9': # needs to replace the font created in the constructor of # QApplication, which is invalid use of QFont but works on Mac QFont.insertSubstitution('.Lucida Grande UI', 'Lucida Grande') self._ui = ui self._mainapp = QApplication(sys.argv) if QT_VERSION >= 0x50000: self._mainapp.setAttribute(Qt.AA_UseHighDpiPixmaps, True) if sys.platform == 'darwin': self._mainapp.setAttribute(Qt.AA_DontShowIconsInMenus, True) 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')) self._repomanager = thgrepo.RepoManager(ui, self) self._reporeleaser = releaser = QSignalMapper(self) releaser.mapped[unicode].connect(self._repomanager.releaseRepoAgent) # stop services after control returns to the main event loop self._mainapp.setQuitOnLastWindowClosed(False) self._mainapp.lastWindowClosed.connect(self._quitGracefully, Qt.QueuedConnection) dlg, reporoot = self._createdialog(dlgfunc, args, opts) self._mainreporoot = reporoot try: if dlg: dlg.show() dlg.raise_() else: if reporoot: self._repomanager.releaseRepoAgent(reporoot) self._mainreporoot = None return -1 if thginithook is not None: thginithook() return self._mainapp.exec_() finally: self._exccatcher.release() self._mainapp = self._ui = None @pyqtSlot() def _quitGracefully(self): # won't be called if the application is quit by BugReport dialog if self._mainreporoot: self._repomanager.releaseRepoAgent(self._mainreporoot) self._mainreporoot = None if self._server: self._server.close() if self._tryQuit(): return self._ui.debug('repositories are closing asynchronously\n') self._repomanager.repositoryClosed.connect(self._tryQuit) QTimer.singleShot(5000, self._mainapp.quit) # in case of bug @pyqtSlot() def _tryQuit(self): if self._repomanager.repoRootPaths(): return False self._mainapp.quit() return True def _fixlibrarypaths(self): # make sure to use the bundled Qt plugins to avoid ABI incompatibility # https://doc.qt.io/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 dlg.setAttribute(Qt.WA_DeleteOnClose) if reporoot: dlg.destroyed.connect(self._reporeleaser.map) self._reporeleaser.setMapping(dlg, reporoot) if dlg is not self._workbench and not dlg.parent(): # keep reference to avoid garbage collection. workbench should # exist when run.dispatch() is called for the second time. assert self._workbench dlg.setParent(self._workbench, dlg.windowFlags()) dlg.show() 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 @pyqtSlot(str) def openRepoInWorkbench(self, uroot): """Show the specified repository in Workbench; reuses the existing Workbench process""" assert self._ui singlewb = self._ui.configbool('tortoisehg', 'workbench.single', True) # only if the server is another process; otherwise it would deadlock if (singlewb and not self._server and connectToExistingWorkbench(hglib.fromunicode(uroot))): return self.showRepoInWorkbench(uroot) 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() + '-' + _ugetuser()) @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-4.5.2/tortoisehg/hgqt/guess.py0000644000175000017500000004053313214542271021363 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os from .qtcore import ( QAbstractTableModel, QModelIndex, QSettings, QThread, QTimer, Qt, pyqtSignal, ) from .qtgui import ( QAbstractItemView, QCheckBox, QDialog, QFrame, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QMessageBox, QPushButton, QSizePolicy, QSlider, QSplitter, QTextBrowser, QToolButton, QTreeView, QVBoxLayout, ) from mercurial import ( hg, patch, similar, ) from ..util import ( hglib, thread2, ) from ..util.i18n import _ from . import ( cmdui, htmlui, qtlib, ) # 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 self.pats = pats self.thread = None self.setWindowTitle(_('Detect Copies/Renames in %s') % repoagent.displayName()) self.setWindowIcon(qtlib.geticon('thg-guess')) 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(qtlib.readByteArray(s, 'guess/geom')) hsplit.restoreState(qtlib.readByteArray(s, 'guess/hsplit-state')) vsplit.restoreState(qtlib.readByteArray(s, 'guess/vsplit-state')) slider.setValue(qtlib.readInt(s, 'guess/simslider') 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] ws = wctx.status(listunknown=True) self.repo.lfstatus = False self.unrevlist.clear() dests = [] for u in ws.unknown: dests.append(u) for a in ws.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) item.setSelected(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.clearProgress() 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(self.repo.ui) 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 = hglib.unidifftext(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)) 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 None if role == Qt.DisplayRole: s = self.rows[index.row()][index.column()] f = self.displayformats[index.column()] return 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 None def headerData(self, col, orientation, role): if role != Qt.DisplayRole or orientation != Qt.Horizontal: return None else: return 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.beginResetModel() self.layoutAboutToBeChanged.emit() self.rows.sort(key=lambda x: x[col], reverse=(order == Qt.DescendingOrder)) self.layoutChanged.emit() self.endResetModel() def isEmpty(self): return not bool(self.rows) class RenameSearchThread(QThread): '''Background thread for searching repository history''' match = pyqtSignal(object) progress = pyqtSignal(str, object, str, str, object) showMessage = pyqtSignal(str) def __init__(self, repo, ufiles, minpct, copies): super(RenameSearchThread, self).__init__() self.repo = hg.repository(hglib.loadui(), 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: 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: ws = wctx.status(listclean=True) srcs = ws.removed + ws.deleted srcs += ws.modified + ws.clean else: ws = wctx.status() srcs = ws.removed + ws.deleted added = [wctx[a] for a in sorted(self.ufiles)] removed = [pctx[a] for a in sorted(srcs) if a in pctx] # do not consider files of zero length added = [fctx for fctx in added if fctx.size() > 0] removed = [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-4.5.2/tortoisehg/hgqt/qdelete.py0000644000175000017500000000356213153775104021666 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( QSettings, Qt, ) from .qtgui import ( QCheckBox, QDialog, QDialogButtonBox, QLabel, QVBoxLayout, ) from ..util.i18n import _ from . import qtlib 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.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) self._readSettings() def _readSettings(self): qs = QSettings() qs.beginGroup('qdelete') self._keepchk.setChecked(qtlib.readBool(qs, 'keep', True)) qs.endGroup() def _writeSettings(self): qs = QSettings() qs.beginGroup('qdelete') qs.setValue('keep', self._keepchk.isChecked()) qs.endGroup() def accept(self): self._writeSettings() super(QDeleteDialog, self).accept() def options(self): return {'keep': self._keepchk.isChecked()} tortoisehg-4.5.2/tortoisehg/hgqt/qtgui.py0000644000175000017500000000641513242607601021367 0ustar sborhosborho00000000000000# qtgui.py - PyQt4/5 compatibility wrapper # # Copyright 2015 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. """Thin compatibility wrapper for QtGui and QtWidgets of Qt5""" from __future__ import absolute_import from .qtcore import QT_API if QT_API == 'PyQt4': # http://pyqt.sourceforge.net/Docs/PyQt5/pyqt4_differences.html from PyQt4.QtGui import * from PyQt4.QtGui import QFileDialog as _QFileDialog class QFileDialog(_QFileDialog): getOpenFileName = _QFileDialog.getOpenFileNameAndFilter getOpenFileNames = _QFileDialog.getOpenFileNamesAndFilter getSaveFileName = _QFileDialog.getSaveFileNameAndFilter class QProxyStyle(QStyle): def __init__(self, style=None): if style is None: style = QApplication.style() style.__class__.__init__(self) self._style = style # Delegate all 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 drawPrimitive(self, *args): return self._style.drawPrimitive(*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) elif QT_API == 'PyQt5': from PyQt5.QtGui import * from PyQt5.QtWidgets import * QStyleOptionViewItemV2 = QStyleOptionViewItem QStyleOptionViewItemV3 = QStyleOptionViewItem QStyleOptionViewItemV4 = QStyleOptionViewItem else: raise RuntimeError('unsupported Qt API: %s' % QT_API) tortoisehg-4.5.2/tortoisehg/hgqt/about.py0000644000175000017500000002042513242076403021345 0ustar sborhosborho00000000000000# 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 """ from __future__ import absolute_import import sys from .qtcore import ( PYQT_VERSION_STR, QSettings, QSize, QT_VERSION_STR, QTimer, QUrl, Qt, pyqtSlot, ) from .qtgui import ( QDialog, QDialogButtonBox, QFont, QLabel, QLayout, QPixmap, QPlainTextEdit, QVBoxLayout, ) from .qtnetwork import ( QNetworkAccessManager, QNetworkRequest, ) from ..util import ( hglib, paths, version, ) from ..util.i18n import _ from . import qtlib class AboutDialog(QDialog): """Dialog for showing info about TortoiseHg""" def __init__(self, parent=None): super(AboutDialog, self).__init__(parent) self.setWindowIcon(qtlib.geticon('thg')) 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) self.logo_lbl.setPixmap(QPixmap(qtlib.iconpath('thg_logo_92x50.png'))) 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-2018 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' % ('https://tortoisehg.bitbucket.io', _('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) bbox = QDialogButtonBox(self) self.license_btn = bbox.addButton(_('&License'), QDialogButtonBox.ResetRole) self.license_btn.setAutoDefault(False) self.license_btn.clicked.connect(self.showLicense) self.close_btn = bbox.addButton(QDialogButtonBox.Close) self.close_btn.setDefault(True) self.close_btn.clicked.connect(self.close) self.vbox.addWidget(bbox) 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 = 'https://tortoisehg.bitbucket.io/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): ld = LicenseDialog(self) ld.exec_() def closeEvent(self, event): if self._newverreply: self._newverreply.abort() self._writesettings() super(AboutDialog, self).closeEvent(event) def _readsettings(self): s = QSettings() self.restoreGeometry(qtlib.readByteArray(s, 'about/geom')) def _writesettings(self): s = QSettings() s.setValue('about/geom', self.saveGeometry()) class LicenseDialog(QDialog): """Dialog for showing the TortoiseHg license""" def __init__(self, parent=None): super(LicenseDialog, self).__init__(parent) self.setWindowIcon(qtlib.geticon('thg')) 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 bbox = QDialogButtonBox(self) self.close_btn = bbox.addButton(QDialogButtonBox.Close) self.close_btn.clicked.connect(self.close) self.vbox = QVBoxLayout() self.vbox.setSpacing(6) self.vbox.addWidget(self.lic_txt) self.vbox.addWidget(bbox) self.setLayout(self.vbox) self._readsettings() def closeEvent(self, event): self._writesettings() super(LicenseDialog, self).closeEvent(event) def _readsettings(self): s = QSettings() self.restoreGeometry(qtlib.readByteArray(s, 'license/geom')) def _writesettings(self): s = QSettings() s.setValue('license/geom', self.saveGeometry()) tortoisehg-4.5.2/tortoisehg/hgqt/wctxcleaner.py0000644000175000017500000001064313150123225022544 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( QObject, QThread, pyqtSignal, pyqtSlot, ) from .qtgui import ( QMessageBox, QWidget, ) from mercurial import ( cmdutil, hg, util, ) from ..util.i18n import _ from . import ( cmdcore, cmdui, qtlib, thgrepo, ) def _checkchanged(repo): try: cmdutil.bailifchanged(repo) return False except util.Abort: return True 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.invalidate() self.repo.invalidatedirstate() 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 = _checkchanged(self.repo) 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-4.5.2/tortoisehg/hgqt/serve.py0000644000175000017500000001766613150123225021365 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import tempfile from .qtcore import ( Qt, pyqtSlot, ) from .qtgui import ( QDialog, QSystemTrayIcon, ) from mercurial import ( error, util, ) from ..util import ( hglib, paths, wconfig, ) from ..util.i18n import _ from . import ( cmdcore, cmdui, qtlib, ) from .serve_ui import Ui_ServeDialog from .webconf import WebconfForm class ServeDialog(QDialog): """Dialog for serving repositories via web""" def __init__(self, ui, webconf, parent=None): super(ServeDialog, self).__init__(parent) self.setWindowFlags((self.windowFlags() | Qt.WindowMinimizeButtonHint) & ~Qt.WindowContextHelpButtonHint) self.setWindowIcon(qtlib.geticon('hg-serve')) self._qui = Ui_ServeDialog() self._qui.setupUi(self) self._initwebconf(webconf) self._initcmd(ui) self._initactions() self._updateform() def _initcmd(self, ui): # TODO: forget old logs? self._log_edit = cmdui.LogWidget(self) self._qui.details_tabs.addTab(self._log_edit, _('Log')) # as of hg 3.0, hgweb does not cooperate with command-server channel self._agent = cmdcore.CmdAgent(ui, self, worker='proc') 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())) def _cmdargs(self): """Build command args to run server""" a = ['serve', '--port', str(self.port), '-v'] 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 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 = hglib.shortreponame(lui) 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(lui, webconf=webconf) try: dlg.setport(int(lui.config('web', 'port', '8000'))) except ValueError: pass if repopath or webconfpath: dlg.start() return dlg tortoisehg-4.5.2/tortoisehg/hgqt/mq.py0000644000175000017500000010445713242076403020660 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import re from .qtcore import ( QAbstractListModel, QByteArray, QMimeData, QModelIndex, QObject, QPoint, QTimer, QUrl, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAbstractItemView, QAction, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDockWidget, QFont, QFrame, QHBoxLayout, QInputDialog, QListView, QMenu, QMessageBox, QPushButton, QToolBar, QToolButton, QVBoxLayout, QWidget, ) from mercurial import error from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, commit, qtlib, qdelete, qfold, qrename, rejects, ) def _checkForRejects(repo, rawoutput, parent=None): """Parse output of qpush/qpop to resolve hunk failure manually""" rejre = re.compile(r'saving rejects to file (.*)\.rej') rejfiles = dict((m.group(1), False) for m in rejre.finditer(rawoutput)) for wfile in sorted(rejfiles): if not os.path.exists(repo.wjoin(wfile)): continue 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) r = dlg.exec_() rejfiles[wfile] = (r == QDialog.Accepted) # empty rejfiles means we failed to parse output message return bool(rejfiles) and all(rejfiles.itervalues()) 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() repoagent = self._repoagent.subRepoAgent(hglib.tounicode(repo.mq.path)) 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 hglib.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 finishRevision(self, rev): revspec = hglib.formatrevspec('qbase::%s', rev) return self._runCommand('qfinish', [revspec], {}) 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 == 2 and self._repoagent: repo = self._repoagent.rawRepo() output = hglib.fromunicode(self._cmdsession.warningString()) if _checkForRejects(repo, output, self.parent()): 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() oldcount = len(self._series) oldindexes = [(oi, self._series[oi.row()]) for oi in self.persistentIndexList()] self._buildCache() mappedindexes = [] # old -> new missngindexes = [] # old for oi, patch in oldindexes: try: ni = self.index(self._series.index(patch), oi.column()) mappedindexes.append((oi, ni)) except ValueError: missngindexes.append(oi) # if no indexes are moved, assume missing ones were renamed if (missngindexes and oldcount == len(self._series) and all(oi.row() == ni.row() for oi, ni in mappedindexes)): mappedindexes.extend((oi, self.index(oi.row(), oi.column())) for oi in missngindexes) del missngindexes[:] for oi, ni in mappedindexes: self.changePersistentIndex(oi, ni) for oi in missngindexes: self.changePersistentIndex(oi, QModelIndex()) 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 topAppliedIndex(self, column=0): """Index of the last applied, i.e. qtip, patch""" for row, patch in enumerate(self._series): if self._statusmap.get(patch) == 'applied': return self.index(row, column) return QModelIndex() 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() cmdline = hglib.buildcmdargs('qreorder', after=after, *patches) cmdline = map(hglib.tounicode, cmdline) self._repoagent.runCommand(cmdline) return True def supportedDropActions(self): return Qt.MoveAction class MQPatchesWidget(QDockWidget): patchSelected = pyqtSignal(str) 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._qpushAct = a = QAction( qtlib.geticon('hg-qpush'), _('Push', 'MQ QPush'), self) a.setToolTip(_('Apply one patch')) self._qpushAllAct = a = QAction( qtlib.geticon('hg-qpush-all'), _('Push all', 'MQ QPush'), self) a.setToolTip(_('Apply all patches')) self._qpopAct = a = QAction( qtlib.geticon('hg-qpop'), _('Pop'), self) a.setToolTip(_('Unapply one patch')) self._qpopAllAct = a = QAction( qtlib.geticon('hg-qpop-all'), _('Pop all'), self) a.setToolTip(_('Unapply all patches')) self._qgotoAct = QAction( qtlib.geticon('hg-qgoto'), _('Go &to Patch'), self) self._qpushMoveAct = a = QAction( qtlib.geticon('hg-qpush-move'), _('&Apply Only This Patch'), self) a.setToolTip(_('Apply only the selected patch')) a.setShortcut('Ctrl+Return') a.setShortcutContext(Qt.WidgetWithChildrenShortcut) self._qfinishAct = a = QAction( qtlib.geticon('qfinish'), _('&Finish Patch'), self) a.setToolTip(_('Move applied patches into repository history')) self._qdeleteAct = a = QAction( qtlib.geticon('hg-qdelete'), _('&Delete Patches...'), self) a.setShortcut('Del') a.setShortcutContext(Qt.WidgetWithChildrenShortcut) a.setToolTip(_('Delete selected patches')) self._qrenameAct = a = QAction(_('Re&name Patch...'), self) a.setShortcut('F2') a.setShortcutContext(Qt.WidgetWithChildrenShortcut) self._setGuardsAct = a = QAction( qtlib.geticon('hg-qguard'), _('Set &Guards...'), self) a.setToolTip(_('Configure guards for selected patch')) tbar = QToolBar(_('Patch Queue Actions Toolbar'), self) tbar.setIconSize(qtlib.smallIconSize()) 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._qpushMoveAct) self.addAction(self._qpushMoveAct) tbar.addSeparator() tbar.addAction(self._qfinishAct) tbar.addAction(self._qdeleteAct) self.addAction(self._qdeleteAct) tbar.addSeparator() self.addAction(self._qrenameAct) 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(qtlib.smallIconSize() * 0.75) 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[str].connect(self._onQQueueActivated) self._queueListWidget.activated.connect(self._onGotoPatch) self._qpushAct.triggered.connect(self._patchActions.pushPatch) self._qpushAllAct.triggered.connect(self._patchActions.pushAllPatches) self._qpopAct.triggered.connect(self._patchActions.popPatch) self._qpopAllAct.triggered.connect(self._patchActions.popAllPatches) self._qgotoAct.triggered.connect(self._onGotoPatch) self._qpushMoveAct.triggered.connect(self._onPushMovePatch) self._qfinishAct.triggered.connect(self._onFinishRevision) self._qdeleteAct.triggered.connect(self._onDelete) self._qrenameAct.triggered.connect(self._onRenamePatch) self._setGuardsAct.triggered.connect(self._onGuardConfigure) 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._qgotoAct) menu.addAction(self._qpushMoveAct) menu.addAction(self._qfinishAct) menu.addAction(self._qdeleteAct) menu.addAction(self._qrenameAct) menu.addAction(self._setGuardsAct) menu.setAttribute(Qt.WA_DeleteOnClose) menu.popup(self._queueListWidget.viewport().mapToGlobal(pos)) def _currentPatchName(self): model = self._queueListWidget.model() index = self._queueListWidget.currentIndex() return model.patchName(index) @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() def _onGotoPatch(self): patch = self._currentPatchName() self._patchActions.gotoPatch(patch) @pyqtSlot() def _onPushMovePatch(self): patch = self._currentPatchName() self._patchActions.pushPatch(patch, move=True) @pyqtSlot() def _onFinishRevision(self): patch = self._currentPatchName() self._patchActions.finishRevision(patch) @pyqtSlot() def _onRenamePatch(self): patch = self._currentPatchName() self._patchActions.renamePatch(patch) @pyqtSlot() def _onPatchSelected(self): patch = self._currentPatchName() if patch: self.patchSelected.emit(patch) @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 = any(model.isApplied(i) for i in indexes) self._qgotoAct.setEnabled(len(indexes) == 1 and indexes[0] != model.topAppliedIndex()) self._qpushMoveAct.setEnabled(len(indexes) == 1 and not anyapplied) self._qfinishAct.setEnabled(len(indexes) == 1 and anyapplied) self._qdeleteAct.setEnabled(len(indexes) > 0 and not anyapplied) self._setGuardsAct.setEnabled(len(indexes) == 1) self._qrenameAct.setEnabled(len(indexes) == 1) @pyqtSlot(str) 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(hglib.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-4.5.2/tortoisehg/hgqt/hgemail.py0000644000175000017500000003765413153775104021662 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import re import tempfile from .qtcore import ( QAbstractTableModel, QModelIndex, QSettings, Qt, pyqtSlot, ) from .qtgui import ( QDialog, QKeySequence, QShortcut, ) from mercurial import ( error, util, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, lexers, qtlib, ) from .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._cmdsession = cmdcore.nullCmdSession() 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(qtlib.readByteArray(s, 'email/geom')) self._qui.intro_changesets_splitter.restoreState( qtlib.readByteArray(s, 'email/intro_changesets_splitter')) 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(qtlib.readStringList(s, 'email/%s_history' % k)) w.setCurrentIndex(-1) # unselect for k in ('body', 'attach', 'inline', 'diffstat'): w = getattr(self._qui, '%s_check' % k) w.setChecked(qtlib.readBool(s, 'email/%s' % k)) 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'] = hglib.compactrevs(self._outgoingrevs) opts['bundle'] = True else: opts['rev'] = hglib.compactrevs(self._revs) fmt = self.getdiffformat() if fmt != 'hg': opts[fmt] = True 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())) # The email dialog is available no matter if patchbomb extension isn't # enabled. The extension name makes it unlikely first-time users # would discover that Mercurial ships with a functioning patch MTA. # Since patchbomb doesn't monkey patch any Mercurial code, it's safe # to enable it on demand. opts['config'] = 'extensions.patchbomb=' 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(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): opts = self._patchbombopts() cmdline = hglib.buildcmdargs('email', **opts) cmd = cmdui.CmdSessionDialog(self) cmd.setWindowTitle(_('Sending Email')) cmd.setLogVisible(False) uih = cmdui.PasswordUiHandler(cmd) # skip "intro" and "diffstat" prompt cmd.setSession(self._repoagent.runCommand(cmdline, uih)) if cmd.exec_() == 0: 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 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) cmdline = hglib.buildcmdargs('email', **opts) self._cmdsession = sess = self._repoagent.runCommand(cmdline) sess.setCaptureOutput(True) sess.commandFinished.connect(self._updatepreview) @pyqtSlot() def _updatepreview(self): msg = hglib.tounicode(str(self._cmdsession.readAll())) 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 None 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 hglib.tounicode(coldata(self._repo.changectx(rev))) return None 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 None return 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-4.5.2/tortoisehg/hgqt/compress.py0000644000175000017500000001172313153775104022074 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( QSettings, QTimer, Qt, pyqtSlot, ) from .qtgui import ( QDialog, QDialogButtonBox, QGroupBox, QLayout, QSizePolicy, QVBoxLayout, ) from ..util.i18n import _ from . import ( cmdui, commit, csinfo, qtlib, wctxcleaner, ) class CompressDialog(QDialog): 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) box.setSizeConstraint(QLayout.SetMinAndMaxSize) self.setLayout(box) style = csinfo.panelstyle(selectable=True) srcb = QGroupBox(_('Compress changesets up to and including')) srcb.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) 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.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) 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._cmdcontrol = cmd = cmdui.CmdSessionControlWidget(self) cmd.finished.connect(self.done) cmd.setLogVisible(True) self.compressbtn = cmd.addButton(_('Compress'), QDialogButtonBox.AcceptRole) self.compressbtn.setEnabled(False) self.compressbtn.clicked.connect(self.compress) self.layout().addWidget(cmd) cmd.showStatusMessage(_('Checking...')) self._wctxcleaner = wctxcleaner.WctxCleaner(repoagent, self) self._wctxcleaner.checkFinished.connect(self._checkCompleted) cmd.linkActivated.connect(self._wctxcleaner.runCleaner) QTimer.singleShot(0, self._wctxcleaner.check) self.resize(480, 340) self.setWindowTitle(_('Compress - %s') % repoagent.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, ' 'shelve to patch, ' 'or discard changes.') else: self.compressbtn.setEnabled(True) txt = _('You may continue the compress') self._cmdcontrol.showStatusMessage(txt) def compress(self): uc = ['update', '--clean', '--rev', str(self.revs[1])] rc = ['revert', '--all', '--rev', str(self.revs[0])] sess = self._repoagent.runCommandSequence([uc, rc], self) self._cmdcontrol.setSession(sess) sess.commandFinished.connect(self.commandFinished) self.compressbtn.setEnabled(sess.isFinished()) @pyqtSlot() def commandFinished(self): self._cmdcontrol.showStatusMessage(_('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) self.compressbtn.setEnabled(self._cmdcontrol.session().isFinished()) def commit(self): tip, base = self.revs revs = [c for c in self.repo.revs('%s::%s' % (base, tip)) if c != base] descs = [self.repo[c].description() for c in revs] self.repo.vfs('cur-message.txt', 'w').write('\n* * *\n'.join(descs)) dlg = commit.CommitDialog(self._repoagent, [], {}, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() self._cmdcontrol.showStatusMessage(_('Compress is complete, old ' 'history untouched')) self.compressbtn.hide() self.storeSettings() def storeSettings(self): s = QSettings() s.setValue('compress/geometry', self.saveGeometry()) def restoreSettings(self): s = QSettings() self.restoreGeometry(qtlib.readByteArray(s, 'compress/geometry')) def reject(self): self._cmdcontrol.reject() tortoisehg-4.5.2/tortoisehg/hgqt/tag.py0000644000175000017500000002611113150123225020775 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( Qt, pyqtSlot, ) from .qtgui import ( QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFormLayout, QFrame, QHBoxLayout, QLabel, QLayout, QLineEdit, QSizePolicy, QVBoxLayout, QWidget, ) from ..util import ( hglib, i18n, ) from ..util.i18n import _ from . import ( cmdcore, qtlib, ) 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 self._cmdsession = cmdcore.nullCmdSession() self.setWindowTitle(_('Tag - %s') % repoagent.displayName()) self.setWindowIcon(qtlib.geticon('hg-tag')) # base layout box base = QVBoxLayout() base.setSpacing(0) base.setContentsMargins(0, 0, 0, 0) base.setSizeConstraint(QLayout.SetMinAndMaxSize) self.setLayout(base) formwidget = QWidget(self) formwidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) form = QFormLayout(fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow) formwidget.setLayout(form) base.addWidget(formwidget) repo = repoagent.rawRepo() 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.setMinimumContentsLength(30) # cut long name 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) optbox = QVBoxLayout() optbox.setSpacing(6) form.addRow(expander, 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) form.addRow(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.setVisible(True) self.sep.setVisible(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 not local: 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() 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 if not local: 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, force=force, 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-4.5.2/tortoisehg/hgqt/postreview.py0000644000175000017500000003474613153775104022462 0ustar sborhosborho00000000000000# 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 https://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 __future__ import absolute_import from .qtcore import ( QSettings, QThread, QUrl, Qt, pyqtSlot, ) from .qtgui import ( QDesktopServices, QDialog, QKeySequence, QLineEdit, QShortcut, ) from mercurial import ( extensions, scmutil, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, qtlib, ) from .hgemail import _ChangesetsModel from .postreview_ui import Ui_PostReviewDialog 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'https://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(qtlib.readByteArray(s, 'reviewboard/geom')) self.qui.publish_immediately_check.setChecked( qtlib.readBool(s, 'reviewboard/publish_immediately_check')) self.qui.outgoing_changes_check.setChecked( qtlib.readBool(s, 'reviewboard/outgoing_changes_check')) self.qui.branch_check.setChecked( qtlib.readBool(s, 'reviewboard/branch_check')) self.qui.update_fields.setChecked( qtlib.readBool(s, 'reviewboard/update_fields')) self.qui.summary_edit.addItems( qtlib.readStringList(s, 'reviewboard/summary_edit_history')) 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 = qtlib.readInt(s, 'reviewboard/repo_id') 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 ctx = self.repo[self.selectedRevs[0]] opts['parent'] = str(ctx.p1().rev()) # 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 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(str, str) 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-4.5.2/tortoisehg/hgqt/qtlib.py0000644000175000017500000013562413242076403021356 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import atexit import cgi import os import posixpath import re import shlex import shutil import sip import stat import subprocess import sys import tempfile import weakref from .qtcore import ( QByteArray, QDir, QEvent, QFile, QObject, QProcess, QSize, QUrl, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QApplication, QComboBox, QCommonStyle, QColor, QDesktopServices, QDialog, QFont, QFrame, QHBoxLayout, QIcon, QInputDialog, QKeySequence, QLabel, QLineEdit, QMessageBox, QPainter, QPalette, QPixmap, QPushButton, QShortcut, QSizePolicy, QStyle, QStyleOptionButton, QVBoxLayout, QWidget, ) from mercurial import ( color, extensions, util, ) from ..util import ( editor, hglib, paths, terminal, ) from ..util.i18n import _ try: import win32con openflags = win32con.CREATE_NO_WINDOW except ImportError: openflags = 0 # largest allowed size for widget, defined in QWIDGETSIZE_MAX = (1 << 24) - 1 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 = 'https://tortoisehg.readthedocs.org/en/latest/' + 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 = hglib.tounicode(path) else: path = unicode(path) if os.name == 'nt' and path.startswith('\\\\'): # network share, special handling because of qt bug 13359 # see https://bugreports.qt.io/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 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: if args: shell = shell + ' ' + util.expandpath(args) # check invalid expression in tortoisehg.shell. we shouldn't apply # string formatting to untrusted value, but too late to change syntax. try: shell % {'root': '', 'reponame': ''} except (KeyError, TypeError, ValueError): # KeyError: "%(invalid)s", TypeError: "%(root)d", ValueError: "%" ErrorMsgBox(_('Failed to open path in terminal'), _('Invalid configuration: %s') % hglib.tounicode(shell)) return shellcmd = shell % {'root': root, 'reponame': reponame} cwd = os.getcwd() try: # 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) if os.name == 'nt': # can't parse shellcmd in POSIX way started = QProcess.startDetached(hglib.tounicode(shellcmd)) else: fullargs = map(hglib.tounicode, 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')) # 'type' argument of QSettings.value() can't be used because: # a) it appears to be broken before PyQt 4.11.x (#4882) # b) it may raise TypeError if a setting has a value of an unexpected type def readBool(qs, key, default=False): """Read the specified value from QSettings and coerce into bool""" v = qs.value(key, default) if isinstance(v, basestring): # qvariant.cpp:qt_convertToBool() return not (v == '0' or v == 'false' or v == '') return bool(v) def readByteArray(qs, key, default=b''): """Read the specified value from QSettings and coerce into QByteArray""" v = qs.value(key, default) if v is None: return QByteArray(default) try: return QByteArray(v) except TypeError: return QByteArray(default) def readInt(qs, key, default=0): """Read the specified value from QSettings and coerce into int""" v = qs.value(key, default) if v is None: return int(default) try: return int(v) except (TypeError, ValueError): return int(default) def readString(qs, key, default=''): """Read the specified value from QSettings and coerce into string""" v = qs.value(key, default) if v is None: return unicode(default) try: return unicode(v) except ValueError: return unicode(default) def readStringList(qs, key, default=()): """Read the specified value from QSettings and coerce into string list""" v = qs.value(key, default) if v is None: return list(default) if isinstance(v, basestring): # qvariant.cpp:convert() return [v] try: return [unicode(e) for e in v] except (TypeError, ValueError): return list(default) 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. # TODO: update ui._styles instead of color._defaultstyles _styles = color._defaultstyles _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', 'log.warning': 'black #ffcccc_background', 'status.deleted': 'red bold', 'ui.error': 'red bold #ffcccc_background', 'ui.warning': 'black bold #ffffaa_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 https://doc.qt.io/qt-4.8/richtext-html-subset.html # and https://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 gettextcoloreffect(labels): """Map labels like "log.date" to foreground color if available""" for l in str(labels).split(): if not l: continue for e in _styles.get(l, '').split(): if e.startswith('#') or e in QColor.colorNames(): return QColor(e) return QColor() 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 = cgi.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/' >>> htmlize('') 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 = hglib.tounicode(ui.config('tortoisehg', 'issue.regex')) issuerepl = hglib.tounicode(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 = hglib.tounicode(desc) buf = '' pos = 0 for m in bodyre.finditer(desc): a, b = m.span() if a >= pos: buf += cgi.escape(desc[pos:a]) pos = b groups = m.groups() if groups[0]: cslink = cgi.escape(groups[0]) buf += '%s' % (cslink, cslink) if groups[1]: urllink = cgi.escape(groups[1]) buf += '%s' % (urllink, urllink) if len(groups) > 4 and groups[4]: issue = cgi.escape(groups[4]) issueparams = groups[4:] try: link = re.sub(r'\{(\d+)\}', lambda m: issueparams[int(m.group(1))], issuerepl) link = cgi.escape(link) buf += '%s' % (link, issue) except IndexError: buf += issue if pos < len(desc): buf += cgi.escape(desc[pos:]) return buf return htmlize _iconcache = {} if getattr(sys, 'frozen', False) and os.name == 'nt': def iconpath(f, *insidef): return posixpath.join(':/icons', f, *insidef) else: def iconpath(f, *insidef): return os.path.join(paths.get_icon_path(), f, *insidef) if hasattr(QIcon, 'hasThemeIcon'): # PyQt>=4.7 def _findthemeicon(name): if QIcon.hasThemeIcon(name): return QIcon.fromTheme(name) else: def _findthemeicon(name): pass def _findcustomicon(name): # let a user set the icon of a custom tool button if os.path.isabs(name): path = name if QFile.exists(path): return QIcon(path) return None # https://specifications.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/actions', '.png'), (QSize(16, 16), '16x16/apps', '.png'), (QSize(16, 16), '16x16/mimetypes', '.png'), (QSize(16, 16), '16x16/status', '.png'), (QSize(22, 22), '22x22/actions', '.png'), (QSize(32, 32), '32x32/actions', '.png'), (QSize(32, 32), '32x32/status', '.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 = iconpath(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 = iconpath(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 _findcustomicon(name) or QIcon()) 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, None) option.rect.moveTo(1, 1) option.state |= state style.drawPrimitive(style.PE_IndicatorCheckBox, option, painter) return pix # On machines with a retina display running OSX (i.e. "darwin"), most icons are # too big because Qt4 does not support retina displays very well. # To fix that we let users force tortoishg to use smaller icons by setting a # THG_RETINA environment variable to True (or any value that mercurial parses # as True. # Whereas on Linux, Qt4 has no support for high dpi displays at all causing # icons to be rendered unusably small. The workaround for that is to render # the icons at double the normal size. # TODO: Remove this hack after upgrading to Qt5. IS_RETINA = util.parsebool(os.environ.get('THG_RETINA', '0')) def _fixIconSizeForRetinaDisplay(s): if IS_RETINA: if sys.platform == 'darwin': if s > 1: s /= 2 elif sys.platform == 'linux2': s *= 2 return s def smallIconSize(): style = QApplication.style() s = style.pixelMetric(QStyle.PM_SmallIconSize) s = _fixIconSizeForRetinaDisplay(s) return QSize(s, s) def toolBarIconSize(): if sys.platform == 'darwin': # most Mac users will have laptop-sized screens and prefer a smaller # toolbar to preserve vertical space. style = QCommonStyle() else: style = QApplication.style() s = style.pixelMetric(QStyle.PM_ToolBarIconSize) s = _fixIconSizeForRetinaDisplay(s) return QSize(s, s) def listviewRetinaIconSize(): return QSize(16, 16) def treeviewRetinaIconSize(): return QSize(16, 16) def barRetinaIconSize(): return QSize(10, 10) 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', '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.button(button_id).setText(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.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 BadCompletionBlocker(QObject): """Disable unexpected inline completion by enter key if selectAll()-ed If the selection state looks in the middle of the completion, QComboBox replaces the edit text by the current completion on enter key pressed. This is wrong in the following scenario: >>> from .qtgui import QKeyEvent >>> combo = QComboBox(editable=True) >>> combo.addItem('history value') >>> combo.setEditText('initial value') >>> combo.lineEdit().selectAll() >>> QApplication.sendEvent( ... combo, QKeyEvent(QEvent.KeyPress, Qt.Key_Enter, Qt.NoModifier)) True >>> str(combo.currentText()) 'history value' In this example, QLineControl picks the first item in the combo box because the completion prefix has not been set. BadCompletionBlocker is intended to work around this problem. >>> combo.installEventFilter(BadCompletionBlocker(combo)) >>> combo.setEditText('initial value') >>> combo.lineEdit().selectAll() >>> QApplication.sendEvent( ... combo, QKeyEvent(QEvent.KeyPress, Qt.Key_Enter, Qt.NoModifier)) True >>> str(combo.currentText()) 'initial value' For details, read QLineControl::processKeyEvent() and complete() of src/gui/widgets/qlinecontrol.cpp. """ def __init__(self, parent): super(BadCompletionBlocker, self).__init__(parent) if not isinstance(parent, QComboBox): raise ValueError('invalid object to watch: %r' % parent) def eventFilter(self, watched, event): if watched is not self.parent(): return super(BadCompletionBlocker, self).eventFilter(watched, event) if (event.type() != QEvent.KeyPress or event.key() not in (Qt.Key_Enter, Qt.Key_Return) or not watched.isEditable()): return False # deselect without completion if all text selected le = watched.lineEdit() if le.selectedText() == le.text(): le.deselect() return False class ActionPushButton(QPushButton): """Button which properties are defined by QAction like QToolButton""" def __init__(self, action, parent=None): super(ActionPushButton, self).__init__(parent) self.setAutoDefault(False) # action won't be used as dialog default self._defaultAction = action self.addAction(action) self.clicked.connect(action.trigger) self._copyActionProps() def actionEvent(self, event): if (event.type() == QEvent.ActionChanged and event.action() is self._defaultAction): self._copyActionProps() super(ActionPushButton, self).actionEvent(event) def _copyActionProps(self): action = self._defaultAction self.setEnabled(action.isEnabled()) self.setText(action.text()) self.setToolTip(action.toolTip()) 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 = QIcon(iconpath('expander-open.png')) self.minus = QIcon(iconpath('expander-close.png')) 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().cacheKey() == self.minus.cacheKey() 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) # same policy as status bar of QMainWindow self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) 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.setVisible(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) 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 deleted: >>> from .qtcore import QEventLoop, QTimer >>> def processDeferredDeletion(): ... loop = QEventLoop() ... QTimer.singleShot(0, loop.quit) ... loop.exec_() >>> dlg1.reject() >>> processDeferredDeletion() >>> 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() >>> processDeferredDeletion() >>> 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)) """ 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, ...] 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) dlg.setAttribute(Qt.WA_DeleteOnClose) dlg.destroyed.connect(self._cleanupdlgs) return dlg # "destroyed" is emitted soon after Python wrapper is deleted @pyqtSlot() def _cleanupdlgs(self): for key, dialogs in self._keytodlgs.items(): livedialogs = [dlg for dlg in dialogs if not sip.isdeleted(dlg)] if livedialogs: self._keytodlgs[key] = livedialogs else: del self._keytodlgs[key] def count(self): return sum(len(dlgs) for dlgs in self._keytodlgs.itervalues()) @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 def reload(self): pass 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 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 = hglib.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 hglib.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-4.5.2/tortoisehg/hgqt/rename.py0000644000175000017500000002057613150123225021502 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import sys from .qtcore import ( pyqtSlot, ) from .qtgui import ( QCheckBox, QFileDialog, QFormLayout, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPushButton, QSizePolicy, ) from mercurial import util from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, manifestmodel, qtlib, ) class RenameWidget(cmdui.AbstractCmdWidget): def __init__(self, repoagent, parent=None, source=None, destination=None, iscopy=False): super(RenameWidget, self).__init__(parent) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self._repoagent = repoagent form = QFormLayout() form.setContentsMargins(0, 0, 0, 0) form.setSpacing(6) self.setLayout(form) # widgets self.src_txt = QLineEdit(source or '') self.src_txt.setMinimumWidth(300) self.src_btn = QPushButton(_('Browse...')) self.dest_txt = QLineEdit(destination or source or '') self.dest_btn = QPushButton(_('Browse...')) # use QCompleter(model, parent) to avoid ownership bug of # QCompleter(parent /TransferBack/) in PyQt<4.11.4 comp = manifestmodel.ManifestCompleter(None, self) comp.setModel(manifestmodel.ManifestModel(repoagent, comp)) for lbl, txt, btn in [ (_('Source:'), self.src_txt, self.src_btn), (_('Destination:'), self.dest_txt, self.dest_btn)]: box = QHBoxLayout() box.addWidget(txt, 1) box.addWidget(btn) form.addRow(lbl, box) txt.setCompleter(comp) self.copy_chk = QCheckBox(_('Copy source -> destination')) form.addRow('', self.copy_chk) # some extras form.addRow(QLabel('')) self.hgcmd_txt = QLineEdit() self.hgcmd_txt.setReadOnly(True) form.addRow(_('Hg command:'), self.hgcmd_txt) self.show_command(self.compose_command()) # 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) # dialog setting self.copy_chk.setChecked(iscopy) self.dest_txt.setFocus() self.setRenameCopy() def setRenameCopy(self): if self.copy_chk.isChecked(): self.msgTitle = _('Copy') self.errTitle = _('Copy Error') else: self.msgTitle = _('Rename') self.errTitle = _('Rename Error') @property def repo(self): return self._repoagent.rawRepo() def source(self): return unicode(self.src_txt.text()) def destination(self): return unicode(self.dest_txt.text()) def _sourceFile(self): root = self._repoagent.rootPath() return os.path.normpath(os.path.join(root, self.source())) def _destinationFile(self): root = self._repoagent.rootPath() return os.path.normpath(os.path.join(root, self.destination())) def src_dest_edited(self): self.show_command(self.compose_command()) self.commandChanged.emit() def src_btn_clicked(self): """Select the source file of folder""" FD = QFileDialog if os.path.isfile(self._sourceFile()): caption = _('Select Source File') path, _filter = FD.getOpenFileName(self, caption, '', '', None, FD.ReadOnly) else: caption = _('Select Source Folder') path = FD.getExistingDirectory(self, caption, '', 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._sourceFile()): caption = _('Select Destination File') else: caption = _('Select Destination Folder') path, _filter = FD.getSaveFileName(self, 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 isCopyCommand(self): return self.copy_chk.isChecked() def copy_chk_toggled(self): self.setRenameCopy() self.show_command(self.compose_command()) self.commandChanged.emit() def isCaseFoldingOnWin(self): fullsrc, fulldest = self._sourceFile(), self._destinationFile() return (fullsrc.upper() == fulldest.upper() and sys.platform == 'win32') def compose_command(self): name = self.isCopyCommand() and 'copy' or 'rename' return hglib.buildcmdargs(name, self.source(), self.destination(), v=True, f=True) def show_command(self, cmdline): self.hgcmd_txt.setText('hg %s' % hglib.prettifycmdline(cmdline)) def canRunCommand(self): src, dest = self.source(), self.destination() return bool(src and dest and src != dest and not (self.isCopyCommand() and self.isCaseFoldingOnWin())) def runCommand(self): # check inputs fullsrc, fulldest = self._sourceFile(), self._destinationFile() if not os.path.exists(fullsrc): qtlib.WarningMsgBox(self.msgTitle, _('Source does not exist.')) return cmdcore.nullCmdSession() if not fullsrc.startswith(self._repoagent.rootPath()): qtlib.ErrorMsgBox(self.errTitle, _('The source must be within the repository tree.')) return cmdcore.nullCmdSession() if not fulldest.startswith(self._repoagent.rootPath()): qtlib.ErrorMsgBox(self.errTitle, _('The destination must be within the repository tree.')) return cmdcore.nullCmdSession() if os.path.isfile(fulldest) 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 cmdcore.nullCmdSession() cmdline = self.compose_command() self.show_command(cmdline) return self._repoagent.runCommand(cmdline, self) class RenameDialog(cmdui.CmdControlDialog): def __init__(self, repoagent, parent=None, source=None, destination=None, iscopy=False): super(RenameDialog, self).__init__(parent) self._repoagent = repoagent self.setWindowIcon(qtlib.geticon('hg-rename')) self.setObjectName('rename') cmdwidget = RenameWidget(repoagent, self, source, destination, iscopy) cmdwidget.commandChanged.connect(self._updateUi) self.setCommandWidget(cmdwidget) self.commandFinished.connect(self._checkKnownError) self._updateUi() @pyqtSlot(int) def _checkKnownError(self, ret): if ret == 1: # occurs if _some_ of the files cannot be copied cmdui.errorMessageBox(self.lastFinishedSession(), self) @pyqtSlot() def _updateUi(self): if self.commandWidget().isCopyCommand(): bt = _('Copy') wt = _('Copy - %s') else: bt = _('Rename') wt = _('Rename - %s') self.setRunButtonText(bt) self.setWindowTitle(wt % self._repoagent.displayName()) tortoisehg-4.5.2/tortoisehg/hgqt/qtnetwork.py0000644000175000017500000000102113150123225022251 0ustar sborhosborho00000000000000# qtnetwork.py - PyQt4/5 compatibility wrapper # # Copyright 2015 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. """Thin compatibility wrapper for QtNetwork""" from __future__ import absolute_import from .qtcore import QT_API if QT_API == 'PyQt4': from PyQt4.QtNetwork import * elif QT_API == 'PyQt5': from PyQt5.QtNetwork import * else: raise RuntimeError('unsupported Qt API: %s' % QT_API) tortoisehg-4.5.2/tortoisehg/hgqt/cmdui.py0000644000175000017500000005615213153775104021347 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import import weakref from .qsci import ( QsciScintilla, ) from .qtcore import ( QSettings, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QColor, QDialog, QDialogButtonBox, QFont, QHBoxLayout, QLabel, QLayout, QLineEdit, QMessageBox, QProgressBar, QSizePolicy, QStatusBar, QVBoxLayout, QWidget, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, qtlib, qscilib, ) 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(str) 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._busyrepos = set() self._busypbar = QProgressBar(self, minimum=0, maximum=0) self.addWidget(self._busypbar) self.setStyleSheet('QStatusBar::item { border: none }') self._updateBusyProgress() @pyqtSlot(str) def showMessage(self, ustr, error=False): self.lbl.setText(ustr) if error: self.lbl.setStyleSheet('QLabel { color: red }') else: self.lbl.setStyleSheet('') def setRepoBusy(self, root, busy): root = unicode(root) if busy: self._busyrepos.add(root) else: self._busyrepos.discard(root) self._updateBusyProgress() def _updateBusyProgress(self): # busy indicator is the last option, which is visible only if no # progress information is available visible = bool(self._busyrepos and not self.topics) self._busypbar.setVisible(visible) if visible: self._busypbar.setMaximumSize(150, self.lbl.sizeHint().height()) @pyqtSlot() def clearProgress(self): keys = self.topics.keys() for key in keys: self._removeProgress(key) @pyqtSlot(str) def clearRepoProgress(self, root): root = unicode(root) keys = [k for k in self.topics if k[0] == root] for key in keys: self._removeProgress(key) def _removeProgress(self, key): pm = self.topics[key] self.removeWidget(pm) pm.setParent(None) del self.topics[key] self._updateBusyProgress() # TODO: migrate to setProgress() API @pyqtSlot(str, object, str, str, 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 key = (root, topic) if pos is None or (not pos and not total): if key in self.topics: self._removeProgress(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 self._updateBusyProgress() 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() @pyqtSlot(cmdcore.ProgressMessage) def setProgress(self, progress): self.progress(*progress) @pyqtSlot(str, cmdcore.ProgressMessage) def setRepoProgress(self, root, progress): self.progress(*(progress + (unicode(root),))) 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 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', 'ui.warning', '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(str, 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), unicode(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 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) def keyPressEvent(self, event): # propagate key events important for dialog if event.key() == Qt.Key_Escape: event.ignore() return super(LogWidget, self).keyPressEvent(event) class InteractiveUiHandler(cmdcore.UiHandler): """Handle user interaction of Mercurial commands with GUI prompt""" # Unlike QObject, "uiparent" does not own this handler def __init__(self, uiparent=None): super(InteractiveUiHandler, self).__init__() self._prompttext = '' self._promptmode = cmdcore.UiHandler.NoInput self._promptdefault = '' self._uiparentref = uiparent and weakref.ref(uiparent) def setPrompt(self, text, mode, default=None): self._prompttext = unicode(text) self._promptmode = mode self._promptdefault = unicode(default or '') def getLineInput(self): mode = self._promptmode if mode == cmdcore.UiHandler.TextInput: return self._getTextInput(QLineEdit.Normal) elif mode == cmdcore.UiHandler.PasswordInput: return self._getTextInput(QLineEdit.Password) elif mode == cmdcore.UiHandler.ChoiceInput: return self._getChoiceInput() else: return '' def _getTextInput(self, echomode): text, ok = qtlib.getTextInput(self._parentWidget(), _('TortoiseHg Prompt'), self._prompttext, echomode) if ok: return text def _getChoiceInput(self): msg, choicepairs = hglib.extractchoices(self._prompttext) dlg = QMessageBox(QMessageBox.Question, _('TortoiseHg Prompt'), msg, QMessageBox.NoButton, self._parentWidget()) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) for r, t in choicepairs: button = dlg.addButton(t, QMessageBox.ActionRole) button.response = r if r == self._promptdefault: dlg.setDefaultButton(button) # cancel button is necessary to close prompt dialog with empty response dlg.addButton(QMessageBox.Cancel).hide() dlg.exec_() button = dlg.clickedButton() if button and dlg.buttonRole(button) == QMessageBox.ActionRole: return button.response def _parentWidget(self): p = self._uiparentref and self._uiparentref() while p and not p.isWidgetType(): p = p.parent() return p class PasswordUiHandler(InteractiveUiHandler): """Handle no user interaction of Mercurial commands but password input""" def getLineInput(self): mode = self._promptmode if mode == cmdcore.UiHandler.PasswordInput: return self._getTextInput(QLineEdit.Password) else: return '' _detailbtntextmap = { # current state: action text False: _('Show Detail'), True: _('Hide Detail')} class CmdSessionControlWidget(QWidget): """Helper widget to implement dialog to run Mercurial commands""" finished = pyqtSignal(int) linkActivated = pyqtSignal(str) logVisibilityChanged = pyqtSignal(bool) # this won't provide commandFinished signal because the client code # should know the running session. def __init__(self, parent=None, logVisible=False): super(CmdSessionControlWidget, self).__init__(parent) vbox = QVBoxLayout() vbox.setSpacing(4) vbox.setContentsMargins(0, 0, 0, 0) # command output area self._outputLog = LogWidget(self) self._outputLog.setVisible(logVisible) vbox.addWidget(self._outputLog, 1) ## status and progress labels self._stbar = ThgStatusBar() self._stbar.setSizeGripEnabled(False) self._stbar.linkActivated.connect(self.linkActivated) vbox.addWidget(self._stbar) # bottom buttons self._buttonbox = 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(_detailbtntextmap[logVisible], QDialogButtonBox.ResetRole) self._detailBtn.setAutoDefault(False) self._detailBtn.setCheckable(True) self._detailBtn.setChecked(logVisible) self._detailBtn.toggled.connect(self.setLogVisible) vbox.addWidget(buttons) self.setLayout(vbox) self._session = cmdcore.nullCmdSession() self._stbar.hide() self._updateSizePolicy() self._updateUi() def session(self): return self._session def setSession(self, sess): """Start watching the given command session""" assert self._session.isFinished() self._session = sess sess.commandFinished.connect(self._onCommandFinished) sess.outputReceived.connect(self._outputLog.appendLog) sess.progressReceived.connect(self._stbar.setProgress) self._cancelBtn.setEnabled(True) self._updateStatus() @pyqtSlot() def abortCommand(self): self._session.abort() self._cancelBtn.setDisabled(True) @pyqtSlot() def _onCommandFinished(self): self._updateStatus() self._stbar.clearProgress() def addButton(self, text, role): """Add custom button which will typically start Mercurial command""" button = self._buttonbox.addButton(text, role) self._updateUi() return button @pyqtSlot() def setFocusToCloseButton(self): self._closeBtn.setFocus() def showStatusMessage(self, message): """Display the given message in status bar; the message remains until the command status is changed""" self._stbar.showMessage(message) self._stbar.show() def isLogVisible(self): return self._outputLog.isVisibleTo(self) @pyqtSlot(bool) def setLogVisible(self, visible): """show/hide command output""" if visible == self.isLogVisible(): return self._outputLog.setVisible(visible) self._detailBtn.setChecked(visible) self._detailBtn.setText(_detailbtntextmap[visible]) self._updateSizePolicy() self.logVisibilityChanged.emit(visible) @pyqtSlot() def reject(self): """Request to close the dialog or abort the running command""" 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() return self.finished.emit(self._session.exitCode()) def _updateSizePolicy(self): if self.testAttribute(Qt.WA_WState_OwnSizePolicy): return if self.isLogVisible(): self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) else: self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setAttribute(Qt.WA_WState_OwnSizePolicy, False) def _updateStatus(self): self._stbar.show() self._updateUi() def _updateUi(self): updateStatusMessage(self._stbar, self._session) self._cancelBtn.setVisible(not self._session.isFinished()) self._closeBtn.setVisible(self._session.isFinished()) class AbstractCmdWidget(QWidget): """Widget to prepare Mercurial command controlled by CmdControlDialog""" # signal to update "Run" button, etc. commandChanged = pyqtSignal() def readSettings(self, qs): pass def writeSettings(self, qs): pass def canRunCommand(self): # True if all command parameters are valid raise NotImplementedError def runCommand(self): # return new CmdSession or nullCmdSession on error raise NotImplementedError class CmdControlDialog(QDialog): """Dialog to run one-shot Mercurial command prepared by embedded widget The embedded widget must implement AbstractCmdWidget or provide signals and methods defined by it. Settings are prefixed by the objectName() group, so you should specify unique name by setObjectName(). You don't need to extend this class unless you want to provide additional public methods/signals, or implement custom error handling. Unlike QDialog, the result code is set to the exit code of the last command. exec_() returns 0 on success. """ commandFinished = pyqtSignal(int) def __init__(self, parent=None): super(CmdControlDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) vbox = QVBoxLayout() vbox.setSizeConstraint(QLayout.SetMinAndMaxSize) self.setLayout(vbox) self.__cmdwidget = None self.__cmdcontrol = cmd = CmdSessionControlWidget(self) cmd.finished.connect(self.done) vbox.addWidget(cmd) self.__runbutton = cmd.addButton(_('&Run'), QDialogButtonBox.AcceptRole) self.__runbutton.clicked.connect(self.runCommand) self.__updateUi() # use __-prefix, name mangling, to avoid name conflicts in derived classes def __readSettings(self): if not self.objectName(): return assert self.__cmdwidget qs = QSettings() qs.beginGroup(self.objectName()) self.__cmdwidget.readSettings(qs) self.restoreGeometry(qtlib.readByteArray(qs, 'geom')) qs.endGroup() def __writeSettings(self): if not self.objectName(): return assert self.__cmdwidget qs = QSettings() qs.beginGroup(self.objectName()) self.__cmdwidget.writeSettings(qs) qs.setValue('geom', self.saveGeometry()) qs.endGroup() def commandWidget(self): return self.__cmdwidget def setCommandWidget(self, widget): oldwidget = self.__cmdwidget if oldwidget is widget: return if oldwidget: oldwidget.commandChanged.disconnect(self.__updateUi) self.layout().removeWidget(oldwidget) oldwidget.setParent(None) self.__cmdwidget = widget if widget: self.layout().insertWidget(0, widget, 1) widget.commandChanged.connect(self.__updateUi) self.__readSettings() self.__fixInitialFocus() self.__updateUi() def __fixInitialFocus(self): if self.focusWidget(): # do not change if already set return # set focus to the first item of the command widget fw = self.__cmdwidget while fw.focusPolicy() == Qt.NoFocus or not fw.isVisibleTo(self): fw = fw.nextInFocusChain() if fw is self.__cmdwidget or fw is self.__cmdcontrol: # no candidate available return fw.setFocus() def runButtonText(self): return self.__runbutton.text() def setRunButtonText(self, text): self.__runbutton.setText(text) def isLogVisible(self): return self.__cmdcontrol.isLogVisible() def setLogVisible(self, visible): self.__cmdcontrol.setLogVisible(visible) def isCommandFinished(self): """True if no pending or running command exists (but might not be ready to run command because of incomplete user input)""" return self.__cmdcontrol.session().isFinished() def canRunCommand(self): """True if everything's ready to run command""" return (bool(self.__cmdwidget) and self.__cmdwidget.canRunCommand() and self.isCommandFinished()) @pyqtSlot() def runCommand(self): if not self.canRunCommand(): return sess = self.__cmdwidget.runCommand() if sess.isFinished(): return self.__cmdcontrol.setSession(sess) sess.commandFinished.connect(self.__onCommandFinished) self.__updateUi() @pyqtSlot(int) def __onCommandFinished(self, ret): self.__updateUi() if ret == 0: self.__runbutton.hide() self.__cmdcontrol.setFocusToCloseButton() elif ret == 255 and not self.__cmdcontrol.session().isAborted(): errorMessageBox(self.__cmdcontrol.session(), self) # handle command-specific error if any self.commandFinished.emit(ret) if ret != 255: self.__writeSettings() if ret == 0 and not self.isLogVisible(): self.__cmdcontrol.reject() def lastFinishedSession(self): """Session of the last executed command; can be used in commandFinished handler""" sess = self.__cmdcontrol.session() if not sess.isFinished(): # do not expose running session because this dialog should have # full responsibility to control running command return cmdcore.nullCmdSession() return sess def reject(self): self.__cmdcontrol.reject() @pyqtSlot() def __updateUi(self): if self.__cmdwidget: self.__cmdwidget.setEnabled(self.isCommandFinished()) self.__runbutton.setEnabled(self.canRunCommand()) class CmdSessionDialog(QDialog): """Dialog to monitor running Mercurial commands Unlike QDialog, the result code is set to the exit code of the last command. exec_() returns 0 on success. """ # this won't provide commandFinished signal because the client code # should know the running session. def __init__(self, parent=None): super(CmdSessionDialog, self).__init__(parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setWindowTitle(_('TortoiseHg Command Dialog')) self.resize(540, 420) vbox = QVBoxLayout() self.setLayout(vbox) vbox.setContentsMargins(5, 5, 5, 5) vbox.setSizeConstraint(QLayout.SetMinAndMaxSize) self._cmdcontrol = cmd = CmdSessionControlWidget(self, logVisible=True) cmd.finished.connect(self.done) vbox.addWidget(cmd) def setSession(self, sess): """Start watching the given command session""" self._cmdcontrol.setSession(sess) sess.commandFinished.connect(self._cmdcontrol.setFocusToCloseButton) def isLogVisible(self): return self._cmdcontrol.isLogVisible() def setLogVisible(self, visible): """show/hide command output""" self._cmdcontrol.setLogVisible(visible) def reject(self): self._cmdcontrol.reject() 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-4.5.2/tortoisehg/hgqt/postreview_ui.py0000644000175000017500000002553713242104460023143 0ustar sborhosborho00000000000000# -*- coding: utf-8 -*- # Form implementation generated from reading ui file '/home/sborho/repos/thg/tortoisehg/hgqt/postreview.ui' # # Created by: PyQt5 UI code generator 5.5.1 # # WARNING! All changes made in this file will be lost! from tortoisehg.util.i18n import _ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_PostReviewDialog(object): def setupUi(self, PostReviewDialog): PostReviewDialog.setObjectName("PostReviewDialog") PostReviewDialog.resize(660, 459) self.verticalLayout_5 = QtWidgets.QVBoxLayout(PostReviewDialog) self.verticalLayout_5.setObjectName("verticalLayout_5") self.verticalLayout = QtWidgets.QVBoxLayout() self.verticalLayout.setObjectName("verticalLayout") self.tab_widget = QtWidgets.QTabWidget(PostReviewDialog) self.tab_widget.setMaximumSize(QtCore.QSize(16777215, 110)) self.tab_widget.setObjectName("tab_widget") self.post_review_tab = QtWidgets.QWidget() self.post_review_tab.setObjectName("post_review_tab") self.formLayout_2 = QtWidgets.QFormLayout(self.post_review_tab) self.formLayout_2.setObjectName("formLayout_2") self.repo_id_label = QtWidgets.QLabel(self.post_review_tab) self.repo_id_label.setObjectName("repo_id_label") self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.repo_id_label) self.repo_id_combo = QtWidgets.QComboBox(self.post_review_tab) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.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(QtWidgets.QComboBox.InsertAtTop) self.repo_id_combo.setObjectName("repo_id_combo") self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.repo_id_combo) self.summary_label = QtWidgets.QLabel(self.post_review_tab) self.summary_label.setObjectName("summary_label") self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.summary_label) self.summary_edit = QtWidgets.QComboBox(self.post_review_tab) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.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(QtWidgets.QComboBox.InsertAtTop) self.summary_edit.setObjectName("summary_edit") self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.summary_edit) self.tab_widget.addTab(self.post_review_tab, "") self.update_review_tab = QtWidgets.QWidget() self.update_review_tab.setObjectName("update_review_tab") self.formLayout_3 = QtWidgets.QFormLayout(self.update_review_tab) self.formLayout_3.setObjectName("formLayout_3") self.review_id_label = QtWidgets.QLabel(self.update_review_tab) self.review_id_label.setObjectName("review_id_label") self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.review_id_label) self.review_id_combo = QtWidgets.QComboBox(self.update_review_tab) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.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(QtWidgets.QComboBox.InsertAtTop) self.review_id_combo.setObjectName("review_id_combo") self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.review_id_combo) self.update_fields = QtWidgets.QCheckBox(self.update_review_tab) self.update_fields.setObjectName("update_fields") self.formLayout_3.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.update_fields) self.tab_widget.addTab(self.update_review_tab, "") self.verticalLayout.addWidget(self.tab_widget) self.options_group = QtWidgets.QGroupBox(PostReviewDialog) self.options_group.setObjectName("options_group") self.gridLayout = QtWidgets.QGridLayout(self.options_group) self.gridLayout.setObjectName("gridLayout") self.outgoing_changes_check = QtWidgets.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 = QtWidgets.QCheckBox(self.options_group) self.branch_check.setObjectName("branch_check") self.gridLayout.addWidget(self.branch_check, 0, 1, 1, 1) self.publish_immediately_check = QtWidgets.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 = QtWidgets.QGroupBox(PostReviewDialog) self.changesets_box.setEnabled(True) self.changesets_box.setToolTip("") self.changesets_box.setObjectName("changesets_box") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.changesets_box) self.verticalLayout_3.setObjectName("verticalLayout_3") self.changesets_view = QtWidgets.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 = QtWidgets.QHBoxLayout() self.dialogbuttons_layout.setObjectName("dialogbuttons_layout") self.settings_button = QtWidgets.QPushButton(PostReviewDialog) self.settings_button.setToolTip("") self.settings_button.setDefault(False) self.settings_button.setObjectName("settings_button") self.dialogbuttons_layout.addWidget(self.settings_button) spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.dialogbuttons_layout.addItem(spacerItem) self.progress_bar = QtWidgets.QProgressBar(PostReviewDialog) self.progress_bar.setMinimumSize(QtCore.QSize(200, 0)) font = QtGui.QFont() font.setKerning(True) 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(QtWidgets.QProgressBar.TopToBottom) self.progress_bar.setObjectName("progress_bar") self.dialogbuttons_layout.addWidget(self.progress_bar) self.progress_label = QtWidgets.QLabel(PostReviewDialog) self.progress_label.setObjectName("progress_label") self.dialogbuttons_layout.addWidget(self.progress_label) spacerItem1 = QtWidgets.QSpacerItem(0, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.dialogbuttons_layout.addItem(spacerItem1) self.post_review_button = QtWidgets.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 = QtWidgets.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) self.post_review_button.clicked.connect(PostReviewDialog.accept) self.settings_button.clicked.connect(PostReviewDialog.onSettingsButtonClicked) self.close_button.clicked.connect(PostReviewDialog.close) self.outgoing_changes_check.toggled['bool'].connect(PostReviewDialog.outgoingChangesCheckToggle) self.branch_check.toggled['bool'].connect(PostReviewDialog.branchCheckToggle) self.tab_widget.currentChanged['int'].connect(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): _translate = QtCore.QCoreApplication.translate 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-4.5.2/tortoisehg/hgqt/sign.py0000644000175000017500000001546313150123225021172 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( Qt, pyqtSlot, ) from .qtgui import ( QCheckBox, QDialog, QDialogButtonBox, QFormLayout, QFrame, QHBoxLayout, QLabel, QLayout, QLineEdit, QSizePolicy, QVBoxLayout, QWidget, ) from ..util import hglib from ..util.i18n import _ from . 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 self._cmdsession = cmdcore.nullCmdSession() self.rev = rev # base layout box base = QVBoxLayout() base.setSpacing(0) base.setContentsMargins(*(0,)*4) base.setSizeConstraint(QLayout.SetMinAndMaxSize) self.setLayout(base) ## main layout grid formwidget = QWidget(self) formwidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) form = QFormLayout(fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow) formwidget.setLayout(form) base.addWidget(formwidget) repo = repoagent.rawRepo() 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) optbox = QVBoxLayout() optbox.setSpacing(6) form.addRow(expander, 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) form.addRow(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') % repoagent.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 = 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.setVisible(True) self.sep.setVisible(True) self.status.set_status(text, icon) def clear_status(self): self.status.setHidden(True) self.sep.setHidden(True) tortoisehg-4.5.2/tortoisehg/hgqt/lfprompt.py0000644000175000017500000000471113150123225022067 0ustar sborhosborho00000000000000# 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 error, match from tortoisehg.hgqt import qtlib from tortoisehg.util import hglib from tortoisehg.util.i18n import _ def _createPrompt(parent, files): return qtlib.CustomPrompt( _('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 = [] section = 'largefiles' try: minsize = float(ui.config(section, 'minsize', default=10)) except ValueError: minsize = 10 patterns = ui.config(section, 'patterns', default=()) if patterns: patterns = patterns.split(' ') try: matcher = match.match(repo.root, '', list(patterns)) except error.Abort as e: qtlib.WarningMsgBox(_('Invalid Patterns'), _('Failed to process largefiles.patterns.'), hglib.tounicode(str(e)), parent=parent) return None 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 try: filesize = os.path.getsize(repo.wjoin(wfile)) if filesize > minsize * 1024 * 1024: lfiles.append(wfile) except OSError: pass # file not exist or inaccessible if lfiles: ret = _createPrompt(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-4.5.2/tortoisehg/hgqt/serve.ui0000644000175000017500000001004613150123225021333 0ustar sborhosborho00000000000000 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-4.5.2/tortoisehg/hgqt/qsci.py0000644000175000017500000000077513150123225021171 0ustar sborhosborho00000000000000# qsci.py - PyQt4/5 compatibility wrapper # # Copyright 2015 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. """Thin compatibility wrapper for Qsci""" from __future__ import absolute_import from .qtcore import QT_API if QT_API == 'PyQt4': from PyQt4.Qsci import * elif QT_API == 'PyQt5': from PyQt5.Qsci import * else: raise RuntimeError('unsupported Qt API: %s' % QT_API) tortoisehg-4.5.2/tortoisehg/hgqt/fileencoding.py0000644000175000017500000001445013150123225022653 0ustar sborhosborho00000000000000# fileencoding.py - utility to handle encoding of file contents in Qt # # Copyright 2014 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. from __future__ import absolute_import import codecs from .qtgui import ( QActionGroup, ) from mercurial import encoding from ..util.i18n import _ # List of encoding names which are likely used, based on the Chromium # source and the Python documentation # . # # - no UTF-16 or -32, which is binary in Mercurial # - no ASCII because it can be represented in other encodings _ENCODINGNAMES = [ ('utf-8', _('Unicode')), ('iso8859-1', _('Western Europe')), ('cp1252', _('Western Europe')), ('gbk', _('Unified Chinese')), ('big5', _('Traditional Chinese')), ('big5hkscs', _('Traditional Chinese')), ('euc_kr', _('Korean')), ('cp932', _('Japanese')), ('euc_jp', _('Japanese')), ('iso2022_jp', _('Japanese')), ('cp874', _('Thai')), ('iso8859-15', _('Western Europe')), ('mac-roman', _('Western Europe')), ('iso8859-2', _('Central and Eastern Europe')), ('cp1250', _('Central and Eastern Europe')), ('iso8859-5', _('Cyrillic')), ('cp1251', _('Cyrillic')), ('koi8-r', _('Russian')), ('koi8-u', _('Ukrainian')), ('iso8859-7', _('Greek')), ('cp1253', _('Greek')), ('cp1254', _('Turkish')), ('cp1256', _('Arabic')), ('iso8859-6', _('Arabic')), ('cp1255', _('Hebrew')), ('iso8859-8', _('Hebrew')), ('cp1258', _('Vietnamese')), ('iso8859-4', _('Baltic')), ('iso8859-13', _('Baltic')), ('cp1257', _('Baltic')), ('iso8859-3', _('Southern Europe')), ('iso8859-10', _('Nordic')), ('iso8859-14', _('Celtic')), ('iso8859-16', _('South-Eastern Europe')), ] # map to wider encoding included in the table _SUBSTMAP = { 'ascii': 'utf-8', 'shift_jis': 'cp932', } # i18n: comma-separated list of common encoding names in your locale, e.g. # "utf-8,shift_jis,euc_jp,iso2022_jp" for "ja" locale. # # for the best guess, put structured encodings like "utf-8" in front, e.g. # "utf-8,iso8859-1" instead of "iso8859-1,utf-8" because "iso8859-1" can # decode arbitrary byte sequence and never fall back. # # pick from the following encodings: # utf-8, iso8859-1, cp1252, gbk, big5, big5hkscs, euc_kr, cp932, euc_jp, # iso2022_jp, cp874, iso8859-15, mac-roman, iso8859-2, cp1250, iso8859-5, # cp1251, koi8-r, koi8-u, iso8859-7, cp1253, cp1254, cp1256, iso8859-6, # cp1255, iso8859-8, cp1258, iso8859-4, iso8859-13, cp1257, iso8859-3, # iso8859-10, iso8859-14, iso8859-16 _LOCALEENCODINGS = _('$FILE_ENCODINGS').replace('$FILE_ENCODINGS', '') def canonname(name): """Resolve aliases and substitutions of the specified encoding >>> canonname('Shift_JIS') 'cp932' >>> canonname('foo') Traceback (most recent call last): ... LookupError: unknown encoding: foo the listed names should be canonicalized: >>> [(enc, canonname(enc)) for enc, _region in _ENCODINGNAMES ... if enc != canonname(enc)] [] """ name = codecs.lookup(name).name return _SUBSTMAP.get(name, name) def contentencoding(ui, fallbackenc=None): """Preferred encoding of file contents in repository""" # assumes web.encoding is the content encoding, not the filename one enc = ui.config('web', 'encoding') if enc: try: return canonname(enc) except LookupError: ui.debug('ignoring invalid web.encoding: %s\n' % enc) return canonname(fallbackenc or encoding.encoding) def knownencodings(): """List of encoding names which are likely used""" return [enc for enc, _region in _ENCODINGNAMES] def _localeencodings(): localeencs = [] if _LOCALEENCODINGS: localeencs.extend(canonname(e) for e in _LOCALEENCODINGS.split(',')) else: # utf-8 is widely used; also mimics pre-2.11 behavior (007047b54911) localeencs.append('utf-8') enc = canonname(encoding.encoding) if enc not in localeencs: localeencs.append(enc) return localeencs def guessencoding(ui, data, fallbackenc=None): """Guess encoding of the specified data from locale-specific candidates This is faster than chardet.detect() and works well for structured encodings like utf-8 or CJK's, but won't be possible to distinguish iso8859 variant. iso8859-1 can decode any byte sequence for example. """ if not isinstance(data, str): raise ValueError('data must be bytes') candidateencs = _localeencodings() prefenc = contentencoding(ui) if prefenc not in candidateencs: candidateencs.insert(0, prefenc) for enc in candidateencs: try: data.decode(enc) return enc except UnicodeDecodeError: pass # fallbackenc can be better than prefenc since prefenc failed if fallbackenc: return canonname(fallbackenc) return prefenc def createActionGroup(parent): group = QActionGroup(parent) for enc, region in _ENCODINGNAMES: a = group.addAction('%s (%s)' % (region, enc)) a.setCheckable(True) a.setData(enc) return group def addCustomAction(group, name): cname = canonname(name) # will raise LookupError for invalid name a = group.addAction(name) a.setCheckable(True) a.setData(cname) return a def addActionsToMenu(menu, group): localeencs = set(_localeencodings()) localeacts = [] otheracts = [] for a in group.actions(): enc = str(a.data()) if enc in localeencs: localeacts.append(a) else: otheracts.append(a) if localeacts: menu.addActions(localeacts) menu.addSeparator() menu.addActions(otheracts) def findActionByName(group, name): cname = canonname(name) for a in group.actions(): if str(a.data()) == cname: return a raise LookupError('no encoding action: %s' % name) def checkedActionName(group): a = group.checkedAction() if not a: return '' return str(a.data()) def checkActionByName(group, name): try: a = findActionByName(group, name) except LookupError: a = addCustomAction(group, name) a.setChecked(True) tortoisehg-4.5.2/tortoisehg/hgqt/locktool.py0000644000175000017500000001755513153775104022100 0ustar sborhosborho00000000000000# locktool.py - TortoiseHg's file locking widget # # Copyright 2016 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. from __future__ import absolute_import import os from .qtcore import ( QModelIndex, QSettings, QTimer, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAction, QDialog, QFileDialog, QKeySequence, QLabel, QToolBar, QTreeWidget, QTreeWidgetItem, QVBoxLayout, ) from mercurial import ( extensions, util, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, qtlib, ) _FILE_FILTER = ';;'.join([ _('Word docs (*.doc *.docx)'), _('PDF docs (*.pdf)'), _('Excel files (*.xls *.xlsx)'), _('All files (*)')]) class LockDialog(QDialog): showMessage = pyqtSignal(str) @property def repo(self): return self._repoagent.rawRepo() def __init__(self, repoagent, parent=None): QDialog.__init__(self, parent) self.setWindowTitle(_('TortoiseHg Lock Tool - %s') % \ repoagent.shortName()) self.setWindowIcon(qtlib.geticon('thg-password')) layout = QVBoxLayout() layout.setContentsMargins(2, 2, 2, 2) layout.setSpacing(4) self.setLayout(layout) self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() self._repoagent.configChanged.connect(self.reload) tb = QToolBar(self) tb.setIconSize(qtlib.toolBarIconSize()) tb.setStyleSheet(qtlib.tbstylesheet) self.layout().addWidget(tb) self.refreshAction = a = QAction(self) a.setToolTip(_('Refresh lock information')) a.setIcon(qtlib.geticon('view-refresh')) a.triggered.connect(self.reload) tb.addAction(a) self.addAction = a = QAction(self) a.setToolTip(_('Lock a file not described in .hglocks')) a.setIcon(qtlib.geticon('new-group')) # TODO: not the best icon a.triggered.connect(self.lockany) tb.addAction(a) self.stopAction = a = QAction(self) a.setToolTip(_('Stop current operation')) a.setIcon(qtlib.geticon('process-stop')) a.triggered.connect(self.stopclicked) tb.addAction(a) lbl = QLabel(_('Locked And Lockable Files:')) self.locktw = tw = QTreeWidget(self) tw.setColumnCount(3) tw.setHeaderLabels([_('Path'), _('Locking User'), _('Purpose')]) tw.setEnabled(False) tw.doubleClicked.connect(self.rowDoubleClicked) layout.addWidget(lbl) layout.addWidget(tw) self._stbar = cmdui.ThgStatusBar() layout.addWidget(self._stbar) self.showMessage.connect(self._stbar.showMessage) s = QSettings() self.restoreGeometry(qtlib.readByteArray(s, 'lock/geom')) self.locktw.header().restoreState( qtlib.readByteArray(s, 'lock/treestate')) QTimer.singleShot(0, self.finishSetup) @pyqtSlot() def finishSetup(self): 'complete the setup, some of these steps might fail' # this code is specific to simplelock try: self.sl = extensions.find('simplelock') except KeyError: qtlib.WarningMsgBox(_('Simplelock extension not enabled'), _('Please enable and configure simplelock'), parent=self) self.reject() return # this code is specific to simplelock self.refillModel() self.reload() def refillModel(self): # this code is specific to simplelock locks = self.sl.parseLocks(self.repo) lockables = self.sl.readlockables(self.repo) for wfile in lockables: if wfile not in locks: locks[wfile] = ['', ''] # this code is specific to simplelock self.locktw.clear() self.rawrows = sorted([(w, u, p) for w, (u, p) in locks.items()]) rows = [] for wfile, user, purpose in self.rawrows: uwfile = hglib.tounicode(wfile) uuser = hglib.tounicode(user) upurpose = hglib.tounicode(purpose) rows.append(QTreeWidgetItem([uwfile, uuser, upurpose])) self.locktw.addTopLevelItems(rows) return locks def reject(self): s = QSettings() s.setValue('lock/geom', self.saveGeometry()) s.setValue('lock/treestate', self.locktw.header().saveState()) QDialog.reject(self) @pyqtSlot() def _updateUi(self): sess = self._cmdsession self.refreshAction.setEnabled(sess.isFinished()) self.addAction.setEnabled(sess.isFinished()) self.locktw.setEnabled(sess.isFinished()) self.stopAction.setEnabled(not sess.isFinished()) @pyqtSlot() def lockany(self): wfile, _filter = QFileDialog.getOpenFileName( self, _('Open a (nonmergable) file you wish to be locked'), self.repo.root, _FILE_FILTER) wfile = util.normpath(unicode(wfile)) pathprefix = util.normpath(hglib.tounicode(self.repo.root)) + '/' if not os.path.normcase(wfile).startswith(os.path.normcase(pathprefix)): self.showMessage.emit(_('File was not within current repository')) wfile = wfile[len(pathprefix):] self.showMessage.emit(_('Locking %s') % wfile) self.lockrun(['lock', wfile]) def unlock(self, wfile): self.showMessage.emit(_('Unlocking %s') % wfile) self.lockrun(['unlock', wfile]) def lock(self, wfile, user): self.showMessage.emit(_('Locking %s') % wfile) self.lockrun(['lock', wfile]) @pyqtSlot() def reload(self): 'update list of locks, then update UI' self.showMessage.emit(_('Refreshing locks...')) self.lockrun(['locks']) # has side-effect of refreshing locks def lockrun(self, ucmdline): self.operation = ucmdline + [None] self._cmdsession = sess = self._repoagent.runCommand(ucmdline, self) sess.commandFinished.connect(self.operationComplete) self._updateUi() def operationComplete(self): locks = self.refillModel() self._updateUi() op, wfile = self.operation[:2] if op == 'lock': if wfile in locks and locks[wfile][1]: self.showMessage.emit(_('Lock of %s successful') % wfile) qtlib.openlocalurl(self.operation[1]) else: self.showMessage.emit(_('Lock of %s failed, retry') % wfile) elif op == 'unlock': if wfile in locks and locks[wfile][1]: self.showMessage.emit(_('Unlock of %s failed, retry') % wfile) else: self.showMessage.emit(_('Unlock of %s successful') % wfile) elif locks: self.showMessage.emit(_('Ready, double click to lock or unlock')) else: self.showMessage.emit(_('Ready')) self.operation = ['N/A', None] @pyqtSlot() def stopclicked(self): self._cmdsession.abort() @pyqtSlot(QModelIndex) def rowDoubleClicked(self, index): wfile, user, purpose = self.rawrows[index.row()] curuser = qtlib.getCurrentUsername(self, self.repo, {}) if user or purpose: if user != curuser: self.showMessage.emit(_('You can only release your own locks')) else: self.unlock(wfile) else: self.lock(wfile, curuser) def canExit(self): return self._cmdsession.isFinished() def keyPressEvent(self, event): sess = self._cmdsession if event.matches(QKeySequence.Refresh): self.reload() elif event.key() == Qt.Key_Escape and not sess.isFinished(): sess.abort() else: return super(LockDialog, self).keyPressEvent(event) tortoisehg-4.5.2/tortoisehg/hgqt/hgemail_ui.py0000644000175000017500000004507113242104460022335 0ustar sborhosborho00000000000000# -*- coding: utf-8 -*- # Form implementation generated from reading ui file '/home/sborho/repos/thg/tortoisehg/hgqt/hgemail.ui' # # Created by: PyQt5 UI code generator 5.5.1 # # WARNING! All changes made in this file will be lost! from tortoisehg.util.i18n import _ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_EmailDialog(object): def setupUi(self, EmailDialog): EmailDialog.setObjectName("EmailDialog") EmailDialog.resize(660, 519) EmailDialog.setSizeGripEnabled(True) self.verticalLayout_5 = QtWidgets.QVBoxLayout(EmailDialog) self.verticalLayout_5.setObjectName("verticalLayout_5") self.main_tabs = QtWidgets.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 = QtWidgets.QWidget() self.edit_tab.setObjectName("edit_tab") self.gridLayout = QtWidgets.QGridLayout(self.edit_tab) self.gridLayout.setObjectName("gridLayout") self.envelope_box = QtWidgets.QGroupBox(self.edit_tab) self.envelope_box.setTitle("") self.envelope_box.setObjectName("envelope_box") self.formLayout = QtWidgets.QFormLayout(self.envelope_box) self.formLayout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow) self.formLayout.setObjectName("formLayout") self.to_label = QtWidgets.QLabel(self.envelope_box) self.to_label.setObjectName("to_label") self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.to_label) self.to_edit = QtWidgets.QComboBox(self.envelope_box) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.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(QtWidgets.QComboBox.InsertAtTop) self.to_edit.setObjectName("to_edit") self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.to_edit) self.cc_label = QtWidgets.QLabel(self.envelope_box) self.cc_label.setObjectName("cc_label") self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.cc_label) self.cc_edit = QtWidgets.QComboBox(self.envelope_box) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.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(QtWidgets.QComboBox.InsertAtTop) self.cc_edit.setObjectName("cc_edit") self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.cc_edit) self.from_label = QtWidgets.QLabel(self.envelope_box) self.from_label.setObjectName("from_label") self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.from_label) self.from_edit = QtWidgets.QComboBox(self.envelope_box) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.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(QtWidgets.QComboBox.InsertAtTop) self.from_edit.setObjectName("from_edit") self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.from_edit) self.inreplyto_label = QtWidgets.QLabel(self.envelope_box) self.inreplyto_label.setObjectName("inreplyto_label") self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.inreplyto_label) self.inreplyto_edit = QtWidgets.QLineEdit(self.envelope_box) self.inreplyto_edit.setObjectName("inreplyto_edit") self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.inreplyto_edit) self.flag_label = QtWidgets.QLabel(self.envelope_box) self.flag_label.setObjectName("flag_label") self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.flag_label) self.flag_edit = QtWidgets.QComboBox(self.envelope_box) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.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(QtWidgets.QComboBox.InsertAtTop) self.flag_edit.setObjectName("flag_edit") self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.flag_edit) self.gridLayout.addWidget(self.envelope_box, 0, 0, 1, 1) self.options_edit = QtWidgets.QGroupBox(self.edit_tab) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.options_edit.sizePolicy().hasHeightForWidth()) self.options_edit.setSizePolicy(sizePolicy) self.options_edit.setTitle("") self.options_edit.setObjectName("options_edit") self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.options_edit) self.verticalLayout_4.setObjectName("verticalLayout_4") self.patch_frame = QtWidgets.QFrame(self.options_edit) self.patch_frame.setFrameShape(QtWidgets.QFrame.NoFrame) self.patch_frame.setFrameShadow(QtWidgets.QFrame.Raised) self.patch_frame.setObjectName("patch_frame") self.verticalLayout = QtWidgets.QVBoxLayout(self.patch_frame) self.verticalLayout.setObjectName("verticalLayout") self.hgpatch_radio = QtWidgets.QRadioButton(self.patch_frame) self.hgpatch_radio.setObjectName("hgpatch_radio") self.verticalLayout.addWidget(self.hgpatch_radio) self.gitpatch_radio = QtWidgets.QRadioButton(self.patch_frame) self.gitpatch_radio.setObjectName("gitpatch_radio") self.verticalLayout.addWidget(self.gitpatch_radio) self.plainpatch_radio = QtWidgets.QRadioButton(self.patch_frame) self.plainpatch_radio.setObjectName("plainpatch_radio") self.verticalLayout.addWidget(self.plainpatch_radio) self.bundle_radio = QtWidgets.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 = QtWidgets.QFrame(self.options_edit) self.extra_frame.setFrameShape(QtWidgets.QFrame.NoFrame) self.extra_frame.setFrameShadow(QtWidgets.QFrame.Raised) self.extra_frame.setObjectName("extra_frame") self.horizontalLayout = QtWidgets.QHBoxLayout(self.extra_frame) self.horizontalLayout.setObjectName("horizontalLayout") self.body_check = QtWidgets.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 = QtWidgets.QCheckBox(self.extra_frame) self.attach_check.setObjectName("attach_check") self.horizontalLayout.addWidget(self.attach_check) self.inline_check = QtWidgets.QCheckBox(self.extra_frame) self.inline_check.setObjectName("inline_check") self.horizontalLayout.addWidget(self.inline_check) self.diffstat_check = QtWidgets.QCheckBox(self.extra_frame) self.diffstat_check.setObjectName("diffstat_check") self.horizontalLayout.addWidget(self.diffstat_check) spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.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 = QtWidgets.QCheckBox(self.edit_tab) self.writeintro_check.setObjectName("writeintro_check") self.gridLayout.addWidget(self.writeintro_check, 1, 0, 1, 2) self.intro_changesets_splitter = QtWidgets.QSplitter(self.edit_tab) self.intro_changesets_splitter.setOrientation(QtCore.Qt.Vertical) self.intro_changesets_splitter.setObjectName("intro_changesets_splitter") self.intro_box = QtWidgets.QGroupBox(self.intro_changesets_splitter) self.intro_box.setTitle("") self.intro_box.setObjectName("intro_box") self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.intro_box) self.verticalLayout_2.setObjectName("verticalLayout_2") self.subject_layout = QtWidgets.QHBoxLayout() self.subject_layout.setObjectName("subject_layout") self.subject_label = QtWidgets.QLabel(self.intro_box) self.subject_label.setObjectName("subject_label") self.subject_layout.addWidget(self.subject_label) self.subject_edit = QtWidgets.QComboBox(self.intro_box) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.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(QtWidgets.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 = QtWidgets.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 = QtWidgets.QGroupBox(self.intro_changesets_splitter) self.changesets_box.setObjectName("changesets_box") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.changesets_box) self.verticalLayout_3.setObjectName("verticalLayout_3") self.changesets_view = QtWidgets.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 = QtWidgets.QHBoxLayout() self.selectallnone_layout.setObjectName("selectallnone_layout") self.selectall_button = QtWidgets.QPushButton(self.changesets_box) self.selectall_button.setObjectName("selectall_button") self.selectallnone_layout.addWidget(self.selectall_button) self.selectnone_button = QtWidgets.QPushButton(self.changesets_box) self.selectnone_button.setObjectName("selectnone_button") self.selectallnone_layout.addWidget(self.selectnone_button) spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.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 = QtWidgets.QWidget() self.preview_tab.setObjectName("preview_tab") self.gridLayout_2 = QtWidgets.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 = QtWidgets.QHBoxLayout() self.dialogbuttons_layout.setObjectName("dialogbuttons_layout") self.settings_button = QtWidgets.QPushButton(EmailDialog) self.settings_button.setToolTip("") self.settings_button.setDefault(False) self.settings_button.setObjectName("settings_button") self.dialogbuttons_layout.addWidget(self.settings_button) spacerItem2 = QtWidgets.QSpacerItem(25, 19, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.dialogbuttons_layout.addItem(spacerItem2) self.send_button = QtWidgets.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 = QtWidgets.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) self.writeintro_check.toggled['bool'].connect(self.intro_box.setVisible) self.send_button.clicked.connect(EmailDialog.accept) self.close_button.clicked.connect(EmailDialog.close) self.writeintro_check.toggled['bool'].connect(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): _translate = QtCore.QCoreApplication.translate 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 PyQt5 import Qsci tortoisehg-4.5.2/tortoisehg/hgqt/revpanel.py0000644000175000017500000001656013251112734022052 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from mercurial import error from ..util import hglib, obsoleteutil from ..util.i18n import _ from . import ( csinfo, qtlib, ) 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-4.5.2/tortoisehg/hgqt/filedialogs.py0000644000175000017500000007546413153775104022537 0ustar sborhosborho00000000000000# 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 """ from __future__ import absolute_import import difflib from .qsci import ( QsciScintilla, ) from .qtcore import ( QEvent, QItemSelectionModel, QPoint, QSettings, QTimer, QUrl, Qt, pyqtSlot, ) from .qtgui import ( QAbstractItemView, QAction, QColor, QDesktopServices, QFrame, QKeySequence, QHBoxLayout, QMainWindow, QMenu, QPainter, QPen, QSplitter, QToolBar, QVBoxLayout, QWidget, ) from ..util import hglib from ..util.i18n import _ from . import ( blockmatcher, filectxactions, fileview, lexers, qtlib, repomodel, repoview, revpanel, ) from .qscilib import Scintilla 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) } def _setupFileMenu(menu, fileactions): for name in ['visualDiff', 'visualDiffToLocal', None, 'visualDiffFile', 'visualDiffFileToLocal', None, 'editFile', 'saveFile', 'editLocalFile', 'revertFile']: if name: menu.addAction(fileactions.action(name)) else: menu.addSeparator() def _fileDataListForSelection(model, selmodel): # since FileRevModel is a model for single file, this creates single # FileData between two revisions instead of a list for each selection indexes = sorted(selmodel.selectedRows()) if not indexes: return [] if len(indexes) == 1: fd = model.fileData(indexes[0]) else: fd = model.fileData(indexes[0], indexes[-1]) return [fd] 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) # Minimal wrapper to make RepoAgent always returns unfiltered repo. Because # filelog_grapher can't take account of hidden changesets, all child widgets # of file dialog need to take unfiltered repo instance. # # TODO: this should be removed if filelog_grapher (and FileRevModel) are # superseded by revset-based implementation. class _UnfilteredRepoAgentProxy(object): def __init__(self, repoagent): self._repoagent = repoagent def rawRepo(self): repo = self._repoagent.rawRepo() return repo.unfiltered() def runCommand(self, cmdline, uihandler=None, overlay=True): cmdline = ['--hidden'] + cmdline return self._repoagent.runCommand(cmdline, uihandler, overlay) def runCommandSequence(self, cmdlines, uihandler=None, overlay=True): cmdlines = [['--hidden'] + l for l in cmdlines] return self._repoagent.runCommandSequence(cmdlines, uihandler, overlay) def __getattr__(self, name): return getattr(self._repoagent, name) class _AbstractFileDialog(QMainWindow): def __init__(self, repoagent, filename): QMainWindow.__init__(self) self._repoagent = _UnfilteredRepoAgentProxy(repoagent) self.setupUi() self._show_rev = None assert not isinstance(filename, unicode) self.filename = filename self.setWindowTitle(_('Hg file log viewer [%s] - %s') % (repoagent.displayName(), hglib.tounicode(filename))) self.setWindowIcon(qtlib.geticon('hg-log')) self.setIconSize(qtlib.toolBarIconSize()) self.createActions() self.setupToolbars() self.setupViews() self.setupModels() @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.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(qtlib.readByteArray(s, 'geom')) self.splitter.restoreState(qtlib.readByteArray(s, 'splitter')) self.revpanel.set_expanded(qtlib.readBool(s, 'revpanel.expanded')) 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() self.repoview.saveSettings() def setupUi(self): 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._repoagent, cs[0], cs, self.splitter) self.contentframe = QFrame(self.splitter) vbox = QVBoxLayout() vbox.setSpacing(0) vbox.setContentsMargins(0, 0, 0, 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 = repomodel.FileRevModel( self._repoagent, self.filename, 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) selmodel = self.repoview.selectionModel() selmodel.selectionChanged.connect(self._onRevisionSelectionChanged) self.filerevmodel.showMessage.connect(self.statusBar().showMessage) QTimer.singleShot(0, self._updateRepoViewForModel) 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, shortcut=QKeySequence.Back, icon=qtlib.geticon('go-previous')) self.actionForward = QAction(_('Forward'), self, enabled=False, shortcut=QKeySequence.Forward, 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) self._fileactions = filectxactions.FilectxActions(self._repoagent, self) self.addActions(self._fileactions.actions()) def _updateFileActions(self): selmodel = self.repoview.selectionModel() selfds = _fileDataListForSelection(self.filerevmodel, selmodel) self._fileactions.setFileDataList(selfds) if len(selmodel.selectedRows()) > 1: texts = {'visualDiff': _('Diff Selected &Changesets'), 'visualDiffFile': _('&Diff Selected File Revisions')} else: texts = {'visualDiff': _('Diff &Changeset to Parent'), 'visualDiffFile': _('&Diff to Parent')} for n, t in texts.iteritems(): self._fileactions.action(n).setText(t) @pyqtSlot() def _updateHistoryActions(self): self.actionBack.setEnabled(self.repoview.canGoBack()) self.actionForward.setEnabled(self.repoview.canGoForward()) @pyqtSlot() def _updateRepoViewForModel(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) 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 menu = QMenu(self) if len(selection) == 2: for name in ['visualDiff', 'visualDiffFile']: menu.addAction(self._fileactions.action(name)) else: _setupFileMenu(menu, self._fileactions) menu.addSeparator() a = menu.addAction(_('Show Revision &Details')) a.setIcon(qtlib.geticon('hg-log')) a.triggered.connect(self.onShowRevisionDetails) menu.setAttribute(Qt.WA_DeleteOnClose) menu.popup(point) def onShowRevisionDetails(self): rev = self.repoview.selectedRevisions()[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(str) 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() fd = self.filerevmodel.fileData(self.repoview.currentIndex()) self.textView.display(fd) self.textView.verticalScrollBar().setValue(pos) self.revpanel.set_revision(rev) self.revpanel.update(repo = self.repo) @pyqtSlot() def _onRevisionSelectionChanged(self): self._checkValidSelection() self._updateFileActions() # 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 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.isValid(): 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) 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() def closeEvent(self, event): self._writeSettings() super(FileDiffDialog, self).closeEvent(event) def _readSettings(self): s = QSettings() s.beginGroup('filediff') try: self.restoreGeometry(qtlib.readByteArray(s, 'geom')) self.splitter.restoreState(qtlib.readByteArray(s, 'splitter')) 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() for w in self._repoViews: w.saveSettings() def setupUi(self): 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() self._repoViews = [] cs = ('fileDiffDialogLeft', _('File Differences Log Columns')) for cfgname in [cs[0], 'fileDiffDialogRight']: w = repoview.HgRepoView(self._repoagent, cfgname, cs, self) w.setSelectionMode(QAbstractItemView.SingleSelection) self.horizontalLayout.addWidget(w) self._repoViews.append(w) self.frame = QFrame() self.splitter.addWidget(layouttowidget(self.horizontalLayout)) self.splitter.addWidget(self.frame) def setupViews(self): # 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 table in self._repoViews: table.setTabKeyNavigation(False) table.installEventFilter(self) table.columnsVisibilityChanged.connect(self._syncColumnsVisibility) 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 = repomodel.FileRevModel( self._repoagent, self.filename, parent=self) for w in self._repoViews: w.setModel(self.filerevmodel) w.menuRequested.connect(self.viewMenuRequest) selmodel = w.selectionModel() selmodel.selectionChanged.connect(self._onRevisionSelectionChanged) QTimer.singleShot(0, self._updateRepoViewForModel) 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) self._fileactions = filectxactions.FilectxActions(self._repoagent, self) self.addActions(self._fileactions.actions()) def _updateFileActionsForSelection(self, selmodel): selfds = _fileDataListForSelection(self.filerevmodel, selmodel) self._fileactions.setFileDataList(selfds) def setupToolbars(self): self.editToolbar.addSeparator() self.editToolbar.addAction(self.actionNextDiff) self.editToolbar.addAction(self.actionPrevDiff) @pyqtSlot() def _updateRepoViewForModel(self): for w in self._repoViews: w.resizeColumns() if self._show_rev is not None: self.goto(self._show_rev) self._show_rev = None elif self._repoViews[0].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 elif watched in self._repoViews: if event.type() == QEvent.FocusIn: self._updateFileActionsForSelection(watched.selectionModel()) 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._repoViews[1]: 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]) @pyqtSlot() def _onRevisionSelectionChanged(self): assert isinstance(self.sender(), QItemSelectionModel) self._updateFileActionsForSelection(self.sender()) def goto(self, rev): index = self.filerevmodel.indexLinkedFromRev(rev) if index.isValid(): if index.row() == 0: index = self.filerevmodel.index(1, 0) self._repoViews[0].setCurrentIndex(index) index = self.filerevmodel.index(0, 0) self._repoViews[1].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() def _syncColumnsVisibility(self): src = self.sender() dest = dict(zip(self._repoViews, reversed(self._repoViews)))[src] dest.setVisibleColumns(src.visibleColumns()) @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 @pyqtSlot(QPoint, object) def viewMenuRequest(self, point, selection): 'User requested a context menu in repo view widget' if not selection: return menu = QMenu(self) _setupFileMenu(menu, self._fileactions) menu.setAttribute(Qt.WA_DeleteOnClose) menu.popup(point) tortoisehg-4.5.2/tortoisehg/hgqt/bugreport.py0000644000175000017500000002415113153775104022251 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import cgi import os import re import sys from .qtcore import ( PYQT_VERSION_STR, QSettings, QT_VERSION_STR, QTimer, QUrl, Qt, pyqtSlot, ) from .qtgui import ( QApplication, QDialog, QDialogButtonBox, QFileDialog, QLabel, QMessageBox, QTextBrowser, QTextOption, QVBoxLayout, qApp, ) from .qtnetwork import ( QNetworkAccessManager, QNetworkRequest, ) from mercurial import ( encoding, extensions, ) from ..util import ( hglib, version, ) from ..util.i18n import _ from . import qtlib try: from .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'https://bitbucket.org/tortoisehg/thg/wiki/BugReport') lbl.setOpenExternalLinks(True) self.layout().addWidget(lbl) tb = QTextBrowser() self.text = self.gettext(opts) tb.setHtml('
    ' + cgi.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 = 'https://tortoisehg.bitbucket.io/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, _filter = 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(qtlib.readByteArray(s, 'bugreport/geom')) 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)] = cgi.escape(hglib.tounicode(val)) try: text = text % msgopts except Exception, e: print e, msgopts else: self._mainlabel = QLabel('%s' % cgi.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(str) 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 tortoisehg.hgqt import qtlib class FakeRepo(object): def __init__(self): self.root = os.getcwd() self.ui = hglib.loadui() 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-4.5.2/tortoisehg/hgqt/status.py0000644000175000017500000012671013153775104021567 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os from .qtcore import ( QAbstractTableModel, QItemSelectionModel, QMimeData, QModelIndex, QObject, QPoint, QSettings, QSize, QThread, QTimer, QUrl, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAbstractItemView, QAction, QCheckBox, QColor, QDialog, QFrame, QHBoxLayout, QKeySequence, QLabel, QLineEdit, QMenu, QPushButton, QShortcut, QSizePolicy, QSplitter, QToolBar, QToolButton, QTreeView, QVBoxLayout, QWidget, ) from mercurial import ( context, error, hg, scmutil, util, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdui, filectxactions, filedata, fileview, qtlib, ) # 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(str) - for status bar titleTextChanged(str) - for window title ''' progress = pyqtSignal(str, object, str, str, object) titleTextChanged = pyqtSignal(str) linkActivated = pyqtSignal(str) showMessage = pyqtSignal(str) fileDisplayed = pyqtSignal(str, str) grepRequested = pyqtSignal(str, 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.setContentsMargins(0, 0, 0, 0) layout.addWidget(split) self.setLayout(layout) vbox = QVBoxLayout() vbox.setContentsMargins(0, 0, 0, 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.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 = StatusFilterActionGroup( 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(qtlib.smallIconSize()) 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( createStatusFilterMenuButton(self.statusfilter, self)) self.filelistToolbar.addSeparator() self.filelistToolbar.addWidget(self.refreshBtn) self._fileactions = filectxactions.WctxActions(self._repoagent, self) self._fileactions.setupCustomToolsMenu('workbench.commit.custom-menu') self._fileactions.linkActivated.connect(self.linkActivated) self._fileactions.refreshNeeded.connect(self.refreshWctx) self._fileactions.runCustomCommandRequested.connect( self.runCustomCommandRequested) self.addActions(self._fileactions.actions()) tv = WctxFileTree(self) 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.setAutoDefault(False) self.clearPatternBtn.setVisible(bool(self.pats)) tv.setAllColumnsShowFocus(True) tv.setContextMenuPolicy(Qt.CustomContextMenu) tv.setDragDropMode(QTreeView.DragOnly) tv.setItemsExpandable(False) tv.setRootIsDecorated(False) tv.setSelectionMode(QTreeView.ExtendedSelection) tv.setTextElideMode(Qt.ElideLeft) tv.sortByColumn(COL_STATUS, Qt.AscendingOrder) tv.doubleClicked.connect(self.onRowDoubleClicked) tv.customContextMenuRequested.connect(self.onMenuRequest) le.textEdited.connect(self.setFilter) self.statusfilter.statusChanged.connect(self.setStatusFilter) self.tv = tv self.le = le self._tvpaletteswitcher = qtlib.PaletteSwitcher(tv) self._togglefileshortcut = a = QShortcut(Qt.Key_Space, tv) a.setContext(Qt.WidgetShortcut) a.setEnabled(False) a.activated.connect(self._toggleSelectedFiles) # 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.setShelveButtonVisible(True) 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.chunkSelectionChanged.connect(self.chunkSelectionChanged) self.fileview.grepRequested.connect(self.grepRequested) 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): name = self._repoagent.displayName() if self.pats: return _('%s - status (selection filtered)') % name else: return _('%s - status') % name def loadSettings(self, qs, prefix): self.fileview.loadSettings(qs, prefix+'/fileview') self.split.restoreState(qtlib.readByteArray(qs, prefix + '/state')) def saveSettings(self, qs, prefix): self.fileview.saveSettings(qs, prefix+'/fileview') qs.setValue(prefix+'/state', self.split.saveState()) def _updatePartials(self, fd): # 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(fd.filePath()) changes = fd.changes 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: fd.setChunkExcluded(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: fd.setChunkExcluded(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) def onMenuRequest(self, point): menu = QMenu(self) selmodel = self.tv.selectionModel() if selmodel and selmodel.hasSelection(): self._setupFileMenu(menu) menu.addSeparator() optmenu = menu.addMenu(_('List Optio&ns')) else: optmenu = menu optmenu.addActions(self.statusfilter.actions()) menu.setAttribute(Qt.WA_DeleteOnClose) menu.popup(self.tv.viewport().mapToGlobal(point)) def _setupFileMenu(self, menu): self._addFileActionsToMenu(menu, [ 'visualDiffFile', 'visualDiffLocalFile', 'copyPatch', 'editLocalFile', 'openLocalFile', 'exploreLocalFile', 'editRejects', None, 'openSubrepo', 'explore', 'terminal', None, 'copyPath', 'editMissingFile', None, 'revertWorkingFile', None, 'navigateFileLog', None, 'forgetFile', 'addFile', 'addLargefile', 'guessRename', 'editHgignore', 'removeFile', 'purgeFile', None, 'markFileAsUnresolved', 'markFileAsResolved']) if self.checkable: menu.addSeparator() # no &-shortcut because check/uncheck can be done by space key menu.addAction(_('Check'), self._checkSelectedFiles) menu.addAction(_('Uncheck'), self._uncheckSelectedFiles) self._addFileActionsToMenu(menu, [ 'editOtherFile', None, 'copyFile', 'renameFile', None, 'customToolsMenu', None, 'renameFileMenu', None, 'remergeFile', None, 'remergeFileMenu']) def _addFileActionsToMenu(self, menu, actnames): for name in actnames: if not name: menu.addSeparator() continue action = self._fileactions.action(name) if action.isEnabled(): menu.addAction(action) 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: assert self.refthread.wstatus is not None self.updateModel(self.refthread.wctx, self.refthread.wstatus, self.refthread.patchecked) self.refthread = None if len(self.repo[None].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(str) 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, wstatus, 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 = hglib.readmergestate(self.repo) tm = WctxModel(self._repoagent, wctx, wstatus, 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() self._togglefileshortcut.setEnabled(True) # Disabled decorator because of bug in older PyQt releases #@pyqtSlot(QModelIndex) def onRowDoubleClicked(self, index): 'tree view emitted a doubleClicked signal, index guarunteed valid' fd = self.tv.model().fileData(index) if fd.subrepoType(): self._fileactions.openSubrepo() elif fd.mergeStatus() == 'U': self._fileactions.remergeFile() elif fd.fileStatus() in set('MAR!'): self._fileactions.visualDiffFile() elif fd.fileStatus() in set('C?'): self._fileactions.editLocalFile() @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(str) def setFilter(self, match): model = self.tv.model() if model: model.setFilter(match) self._tvpaletteswitcher.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(str, 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: del self.partials[wfile] if wfile == hglib.fromunicode(self.fileview.filePath()): self.onCurrentChange(self.tv.currentIndex()) 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 [] @pyqtSlot() def onSelectionChange(self): model = self.tv.model() selmodel = self.tv.selectionModel() selfds = map(model.fileData, selmodel.selectedRows()) self._fileactions.setFileDataList(selfds) # Disabled decorator because of bug in older PyQt releases #@pyqtSlot(QModelIndex) def onCurrentChange(self, index): 'Connected to treeview "currentChanged" signal' changeselect = self.fileview.isChangeSelectionEnabled() model = self.tv.model() fd = model.fileData(index) fd.load(changeselect) if changeselect and not fd.isNull() and not fd.subrepoType(): self._updatePartials(fd) self.fileview.display(fd) def _setCheckStateOfSelectedFiles(self, value): model = self.tv.model() selmodel = self.tv.selectionModel() for index in selmodel.selectedRows(COL_PATH): model.setData(index, value, Qt.CheckStateRole) @pyqtSlot() def _checkSelectedFiles(self): self._setCheckStateOfSelectedFiles(Qt.Checked) @pyqtSlot() def _uncheckSelectedFiles(self): self._setCheckStateOfSelectedFiles(Qt.Unchecked) @pyqtSlot() def _toggleSelectedFiles(self): model = self.tv.model() selmodel = self.tv.selectionModel() for index in selmodel.selectedRows(COL_PATH): if model.data(index, Qt.CheckStateRole) == Qt.Checked: newvalue = Qt.Unchecked else: newvalue = Qt.Checked model.setData(index, newvalue, Qt.CheckStateRole) class StatusThread(QThread): '''Background thread for generating a workingctx''' showMessage = pyqtSignal(str) 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.wstatus = None self.patchecked = {} def run(self): 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.lfstatus = True status = self.repo.status(match=m, **stopts) 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.lfstatus = True status = self.repo.status(node1=self.pctx.p1().node(), **stopts) self.repo.lfstatus = False wctx = context.workingctx(self.repo, changes=status) else: self.repo.lfstatus = True status = self.repo.status(**stopts) self.repo.lfstatus = False wctx = context.workingctx(self.repo, changes=status) self.wctx = wctx self.wstatus = status 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): 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) class WctxModel(QAbstractTableModel): checkCountChanged = pyqtSignal() checkToggled = pyqtSignal(str, bool) def __init__(self, repoagent, wctx, wstatus, ms, pctx, savechecks, opts, checked, parent, checkable=True, defcheck='MAR!S'): QAbstractTableModel.__init__(self, parent) self._repoagent = repoagent self._pctx = pctx 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 wstatus.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 wstatus.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 wstatus.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 wstatus.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 wstatus.unknown or []: nchecked[u] = checked.get(u, '?' in defcheck) rows.append(mkrow(u, '?')) if opts['ignored']: for i in wstatus.ignored or []: nchecked[i] = checked.get(i, 'I' in defcheck) rows.append(mkrow(i, 'I')) if opts['clean']: for c in wstatus.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 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 None if index.column() == COL_PATH: if role == Qt.CheckStateRole and self.checkable: path = self.rows[index.row()][0] 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 "" elif role == Qt.ToolTipRole: return _('Checked count: %d') % self.checkCount elif role == Qt.DisplayRole: return self.rows[index.row()][index.column()] elif role == Qt.TextColorRole: path, status, mst, upath, ext, sz = self.rows[index.row()] if mst: return _colors.get(mst.lower(), QColor('black')) else: return _colors.get(status, QColor('black')) elif role == Qt.ToolTipRole: path, status, mst, upath, ext, sz = self.rows[index.row()] return 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 ico ''' return None def setData(self, index, value, role=Qt.EditRole): if not index.isValid(): return False if (index.column() == COL_PATH and role == Qt.CheckStateRole and self.checkable): if self.data(index, role) == value: return True if value not in (Qt.Checked, Qt.Unchecked): # Qt.PartiallyChecked cannot be set explicitly return False path = self.rows[index.row()][COL_PATH] upath = self.rows[index.row()][COL_PATH_DISPLAY] self.checked[path] = checked = (value == Qt.Checked) self.checkToggled.emit(upath, checked) self.checkCountChanged.emit() self.dataChanged.emit(index, index) return True return False def headerData(self, col, orientation, role): if role != Qt.DisplayRole or orientation != Qt.Horizontal: return None else: return 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 def mimeTypes(self): return ['text/uri-list'] def mimeData(self, indexes): repo = self._repoagent.rawRepo() urls = [] for index in indexes: if index.column() != 0: continue path = self.rows[index.row()][COL_PATH] urls.append(QUrl.fromLocalFile(hglib.tounicode(repo.wjoin(path)))) data = QMimeData() data.setUrls(urls) return data # Custom methods def anyMerge(self): for r in self.rows: if r[COL_MERGE_STATE]: return True return False @util.propertycache def workingContext(self): repo = self._repoagent.rawRepo() return repo[None] def fileData(self, index): """Returns the displayable file data at the given index""" repo = self._repoagent.rawRepo() if not index.isValid(): return filedata.createNullData(repo) path, status, mst, upath, ext, sz = self.rows[index.row()] wfile = util.pconvert(path) ctx = self.workingContext pctx = self._pctx and self._pctx.p1() or ctx.p1() if status == 'S': return filedata.createSubrepoData(ctx, pctx, wfile) else: return filedata.createFileData(ctx, pctx, wfile, status, None, mst) def getRow(self, index): assert index.isValid() return self.rows[index.row()] def getAllRows(self): for row in self.rows: yield row def sort(self, col, order): self.layoutAboutToBeChanged.emit() self.beginResetModel() 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.endResetModel() def setFilter(self, match): 'simple match in filename filter' self.layoutAboutToBeChanged.emit() self.beginResetModel() self.rows = [r for r in self.unfiltered if unicode(match) in r[COL_PATH_DISPLAY]] self.layoutChanged.emit() self.endResetModel() 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', 'hg-modified', _('%s is modified'), 'status.modified', _('modified')), 'A' : StatusType('added', 'hg-add', _('%s is added'), 'status.added', _('added')), 'R' : StatusType('removed', 'hg-removed', _('%s is removed'), 'status.removed', _('removed')), '?' : StatusType('unknown', '', _('%s is not tracked (unknown)'), 'status.unknown', _('unknown')), '!' : StatusType('deleted', '', _('%s is deleted by non-hg command, but still tracked'), 'status.deleted', _('missing')), 'I' : StatusType('ignored', '', _('%s is ignored'), 'status.ignored', _('ignored')), 'C' : StatusType('clean', '', _('%s is not modified (clean)'), 'status.clean', _('clean')), 'S' : StatusType('subrepo', 'thg-subrepo', _('%s is a dirty subrepo'), 'status.subrepo', _('subrepo')), } class StatusFilterActionGroup(QObject): """Actions to switch status filter""" statusChanged = pyqtSignal(str) def __init__(self, statustext, types=None, parent=None): super(StatusFilterActionGroup, self).__init__(parent) self._TYPES = 'MARSC' if types is not None: self._TYPES = types self._actions = {} for c in self._TYPES: st = statusTypes[c] a = QAction('&%s %s' % (c, st.trname), self) a.setCheckable(True) a.setChecked(c in statustext) a.toggled.connect(self._update) self._actions[c] = a @pyqtSlot() def _update(self): self.statusChanged.emit(self.status()) def actions(self): return [self._actions[c] for c in self._TYPES] def isChecked(self, c): return self._actions[c].isChecked() def setChecked(self, c, checked): self._actions[c].setChecked(checked) 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 all(c in self._TYPES for c in text) for c in self._TYPES: self._actions[c].setChecked(c in text) def createStatusFilterMenuButton(actiongroup, parent=None): """Create button with drop-down menu for status filter""" button = QToolButton(parent) button.setIcon(qtlib.geticon('hg-status')) button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(button) menu.addActions(actiongroup.actions()) button.setMenu(menu) return button class StatusDialog(QDialog): 'Standalone status browser' def __init__(self, repoagent, pats, opts, parent=None): QDialog.__init__(self, parent) self.setWindowIcon(qtlib.geticon('hg-status')) self._repoagent = repoagent 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): repoagent = self._repoagent.subRepoAgent(uroot) return StatusDialog(repoagent, [], {}, parent=self) def loadSettings(self): s = QSettings() self.stwidget.loadSettings(s, 'status') self.restoreGeometry(qtlib.readByteArray(s, 'status/geom')) 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-4.5.2/tortoisehg/hgqt/sync.py0000644000175000017500000020032513153775104021213 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import re import tempfile from .qtcore import ( QAbstractTableModel, QDir, QMimeData, QModelIndex, QPoint, QSettings, QTimer, QUrl, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAction, QApplication, QCheckBox, QComboBox, QDesktopServices, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QFrame, QGroupBox, QHBoxLayout, QKeySequence, QLabel, QLineEdit, QMenu, QPushButton, QRadioButton, QSizePolicy, QStackedLayout, QStyle, QToolBar, QTreeView, QVBoxLayout, QWidget, ) from mercurial import ( hg, httpconnection, util, ) from ..util import ( hglib, paths, wconfig, ) from ..util.i18n import _ from . import ( bookmark, cmdcore, cmdui, hgemail, hgrcutil, qtlib, rebase, resolve, thgrepo, ) 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 # ignore preceding white spaces because ui.prompt() for username/password # writes extra " "s to the output channel. (hg 3.1) _extractnodeids = re.compile(r'^\s*([0-9a-f]{40})$', re.MULTILINE).findall class SyncWidget(QWidget, qtlib.TaskWidget): newCommand = pyqtSignal(cmdcore.CmdSession) outgoingNodes = pyqtSignal(object) incomingBundle = pyqtSignal(str, str) showMessage = pyqtSignal(str) pullCompleted = pyqtSignal() pushCompleted = pyqtSignal() switchToRequest = pyqtSignal(str) def __init__(self, repoagent, parent=None): QWidget.__init__(self, parent) layout = QVBoxLayout() layout.setContentsMargins(2, 2, 2, 2) layout.setSpacing(4) self.setLayout(layout) self.setAcceptDrops(True) self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() self._lasturl = None # peer repository of last command self._lastbfile = None # output bundle of last incoming command self.opts = {} self.cmenu = None s = QSettings() for opt in ('force', 'new-branch', 'noproxy', 'debug', 'mq'): val = qtlib.readBool(s, 'sync/' + opt) if val: if opt != 'mq' or 'mq' in self.repo.extensions(): self.opts[opt] = val for opt in ('remotecmd', 'branch'): val = hglib.fromunicode(qtlib.readString(s, 'sync/' + opt)) if val: self.opts[opt] = val self._repoagent.configChanged.connect(self.reload) self._repoagent.repositoryChanged.connect(self._onRepositoryChanged) tb = QToolBar(self) tb.setIconSize(qtlib.toolBarIconSize()) 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(_('Sync Bookmarks'), 'thg-sync-bookmarks', self.syncBookmark) 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) 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) tb.addSeparator() tb.addWidget(self.targetcheckbox) tb.addWidget(self.targetcombo) hbox = QHBoxLayout() hbox.setContentsMargins(0, 0, 0, 0) layout.addLayout(hbox) self.optionshdrlabel = lbl = QLabel(_('Selected Options:')) hbox.addWidget(lbl) self.optionslabel = QLabel() self.optionslabel.setAcceptDrops(False) hbox.addWidget(self.optionslabel) hbox.addStretch() self.pathEditToolbar = tbar = QToolBar(_('Path Edit Toolbar')) tbar.setStyleSheet(qtlib.tbstylesheet) tbar.setIconSize(qtlib.smallIconSize()) layout.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) self.urlentry.returnPressed.connect(self.saveclicked) tbar.addWidget(self.urlentry) tbar.addWidget(qtlib.Spacer(2, 2)) self.browsebutton = QPushButton(_('Browse...')) self.browsebutton.setAutoDefault(False) self.browsebutton.clicked.connect(self._browseUrl) tbar.addWidget(self.browsebutton) # 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) layout.addLayout(hbox, 1) self.savebutton.triggered.connect(self.saveclicked) self.securebutton.triggered.connect(self.secureclicked) self.postpullbutton.clicked.connect(self.postpullclicked) self.optionsbutton.clicked.connect(self.editOptions) 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('') self._updateUi() @property def repo(self): return self._repoagent.rawRepo() def canswitch(self): return False def _loadTargets(self): self.targetcombo.clear() # itemData(role=UserRole) is the argument list to pass to hg self.targetcombo.addItem('', ('--rev', 'null')) # placeholder for name in hglib.namedbranches(self.repo): uname = hglib.tounicode(name) self.targetcombo.addItem(_('branch: ') + uname, ('--branch', name)) self.targetcombo.setItemData(self.targetcombo.count() - 1, name, Qt.ToolTipRole) for name in sorted(self.repo._bookmarks): uname = hglib.tounicode(name) self.targetcombo.addItem(_('bookmark: ') + uname, ('--bookmark', name)) self.targetcombo.setItemData(self.targetcombo.count() - 1, name, Qt.ToolTipRole) def _findTargetIndex(self, ctx): for name in ctx.bookmarks(): uname = hglib.tounicode(name) return self.targetcombo.findText(_('bookmark: ') + uname) if ctx.node() in self.repo.branchheads(ctx.branch()): uname = hglib.tounicode(ctx.branch()) return self.targetcombo.findText(_('branch: ') + uname) return 0 def refreshTargets(self, rev): if type(rev) is not int: return if rev >= len(self.repo): return ctx = self.repo.changectx(rev) if self.targetcombo.count() <= 0: self._loadTargets() self.targetcombo.setItemText(0, _('rev: %d (%s)') % (ctx.rev(), ctx)) self.targetcombo.setItemData(0, ('--rev', str(ctx.rev()))) self.targetcombo.setCurrentIndex(self._findTargetIndex(ctx)) def isTargetSelected(self): return self.targetcheckbox.isChecked() @pyqtSlot(int) def _onRepositoryChanged(self, flags): if flags & thgrepo.LogChanged: self._loadTargets() def editOptions(self): dlg = OptionsDialog(self._repoagent, 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.vfs.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 = {} repoid = hglib.repoidnode(self.repo) for root, shortname in thgrepo.relatedRepositories(repoid): 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) 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 self._cmdsession.isFinished() @pyqtSlot(QPoint, str, str, 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, 'hg-strip')): 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._repoagent, 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 keyPressEvent(self, event): sess = self._cmdsession if event.matches(QKeySequence.Refresh): self.reload() elif event.key() == Qt.Key_Escape and not sess.isFinished(): sess.abort() else: return super(SyncWidget, self).keyPressEvent(event) def stopclicked(self): self._cmdsession.abort() def saveclicked(self): if self.curalias: alias = self.curalias elif 'default' not in self.paths: alias = 'default' else: alias = 'new' dlg = SaveDialog(self._repoagent, 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() @pyqtSlot() def _browseUrl(self): FD = QFileDialog caption = _("Select repository") path = FD.getExistingDirectory(self, caption, self.urlentry.text()) if path: self.urlentry.setText(QDir.toNativeSeparators(path)) 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._repoagent, self.currentUrl(), self) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) dlg.exec_() @pyqtSlot() def _updateUi(self): sess = self._cmdsession for b in self.opbuttons: b.setEnabled(sess.isFinished()) self.stopAction.setEnabled(not sess.isFinished()) def run(self, cmdline, details): if not self._cmdsession.isFinished(): return cmdcore.nullCmdSession() 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.targetcheckbox.isChecked(): idx = self.targetcombo.currentIndex() if idx != -1: args = self.targetcombo.itemData(idx) 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 cmdcore.nullCmdSession() 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() if not self.opts.get('mq'): cmdline.append(lurl) ucmdline = map(hglib.tounicode, cmdline) # bypass overlay of incoming bundle to pull changes overlay = ucmdline[0] not in ('fetch', 'incoming', 'pull') self._cmdsession = sess = self._repoagent.runCommand(ucmdline, self, overlay=overlay) sess.commandFinished.connect(self._updateUi) self._lasturl = cururl self._updateUi() self.newCommand.emit(sess) return sess ## ## Workbench toolbar buttons ## def incoming(self): if not self._cmdsession.isFinished(): self.showMessage.emit(_('sync command already running')) else: self.inclicked() def pull(self): if not self._cmdsession.isFinished(): self.showMessage.emit(_('sync command already running')) else: self.pullclicked() def outgoing(self): if not self._cmdsession.isFinished(): self.showMessage.emit(_('sync command already running')) else: self.outclicked() def push(self, confirm, **kwargs): if not self._cmdsession.isFinished(): self.showMessage.emit(_('sync command already running')) else: self.pushclicked(confirm, **kwargs) def pullBundle(self, bundle, rev, bsource=None): 'accept bundle changesets' if not self._cmdsession.isFinished(): self.showMessage.emit(_('sync command already running')) return save = self.currentUrl() orev = self.opts.get('rev') # XXX hack to ignore incoming bundle because it can't apply phase # movement, pull bookmarks and largefiles. further cleanups should # go on default branch. self.setEditUrl(bsource or 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.targetcheckbox.isChecked(): link += u" (%s)" % self.targetcombo.currentText() return link def inclicked(self): url = self.currentUrl() link = self.linkifyWithTarget(url) if not url.startswith('p4://'): bfile = hglib.fromunicode(url) for badchar in (':', '*', '\\', '?', '#'): bfile = bfile.replace(badchar, '') bfile = bfile.replace('/', '_') bfile = tempfile.mktemp('.hg', bfile+'_', qtlib.gettempdir()) self._lastbfile = hglib.tounicode(bfile) cmdline = ['incoming', '--quiet', '--bundle', bfile] sess = self.run(cmdline, ('force', 'branch', 'rev')) sess.commandFinished.connect(self._onIncomingFinished) else: self._lastbfile = None cmdline = ['incoming'] sess = self.run(cmdline, ('force', 'branch', 'rev')) sess.commandFinished.connect(self._onIncomingFinished) self.showMessage.emit(_('Getting incoming changesets from %s...') % link) @pyqtSlot(int) def _onIncomingFinished(self, ret): link = self.linkifyWithTarget(self._lasturl) if ret == 0: self.showMessage.emit(_('Found incoming changesets from %s') % link) if self._lastbfile and os.path.exists(self._lastbfile): self.incomingBundle.emit(self._lastbfile, self._lasturl) elif ret == 1: self.showMessage.emit(_('No incoming changesets from %s') % link) else: self.showMessage.emit(_('Incoming from %s aborted, ret %d') % (link, ret)) def pullclicked(self, url=None): link = self.linkifyWithTarget(url or self.currentUrl()) cmdline = ['pull', '--verbose'] uimerge = self.repo.ui.configbool('tortoisehg', 'autoresolve', True) \ 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[0] = 'fetch' elif self.opts.get('mq'): # force the tool to update to the pulled changeset cmdline += ['--update', '--config', uimerge] sess = self.run(cmdline, ('force', 'branch', 'rev', 'bookmark', 'mq')) sess.commandFinished.connect(self._onPullFinished) self.showMessage.emit(_('Pulling from %s...') % link) if url: self._lasturl = url # overwrite by user-visible (source) URL @pyqtSlot(int) def _onPullFinished(self, ret): link = self.linkifyWithTarget(self._lasturl) 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.cachedpp in ('rebase', 'updateorrebase'): if os.path.exists(self.repo.vfs.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.exec_() return def outclicked(self): link = self.linkifyWithTarget(self.currentUrl()) cmdline = ['outgoing', '--template', '{node}\n'] sess = self.run(cmdline, ('force', 'branch', 'rev')) sess.setCaptureOutput(True) sess.commandFinished.connect(self._onOutgoingFinished) self.showMessage.emit(_('Finding outgoing changesets to %s...') % link) @pyqtSlot(int) def _onOutgoingFinished(self, ret): link = self.linkifyWithTarget(self._lasturl) if ret == 0: data = str(self._cmdsession.readAll()) nodes = _extractnodeids(data) self.showMessage.emit(_('%d outgoing changesets to %s') % (len(nodes), link)) self.outgoingNodes.emit(nodes) elif ret == 1: self.showMessage.emit(_('No outgoing changesets to %s') % link) else: self.showMessage.emit(_('Outgoing to %s aborted, ret %d') % (link, ret)) def p4pending(self): sess = self.run(['p4pending', '--verbose'], ()) sess.setCaptureOutput(True) sess.commandFinished.connect(self._onP4pendingFinished) self.showMessage.emit(_('Perforce pending...')) @pyqtSlot(int) def _onP4pendingFinished(self, ret): pending = {} if ret == 0: output = str(self._cmdsession.readAll()) for line in output.splitlines(): 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 p4url = hglib.fromunicode(self._lasturl) dlg = PerforcePending(self._repoagent, pending, p4url, self) dlg.showMessage.connect(self.showMessage) dlg.exec_() 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', 'rev', 'bookmark', 'mq') 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.newCommand.emit(cmdcore.nullCmdSession()) self.showMessage.emit(_('Push to %s aborted') % link) self.pushCompleted.emit() return # Precedence of conflicting revision specifiers: # # rev bra all description # --- --- --- -------------------------------------------------------- # x x x 1. method arguments (temporarily set by context menu) # x 2. target combobox (temporarily set) # x 3. opts table (set by OptionsDialog, saved in QSettings) # x x x 4. tortoisehg.defaultpush (saved in hgrc) # # Note: "pushall" is set to True even if 2. or 3. is specified if branch is None: branch = self.opts.get('branch') if not pushall and rev is None and branch is None: defaultpush = self.repo.ui.config('tortoisehg', 'defaultpush', 'all') if self.targetcheckbox.isChecked(): pass elif defaultpush == 'all': # This is the default pass elif defaultpush == 'branch': branch = '.' elif defaultpush == 'revision': rev = '.' else: self.newCommand.emit(cmdcore.nullCmdSession()) self.showMessage.emit(_('Invalid default push revision: %s. ' 'Please check your Mercurial ' 'configuration ' '(tortoisehg.defaultpush)') % defaultpush) self.pushCompleted.emit() return cmdline = ['push'] if rev: cmdline.extend(['--rev', str(rev)]) if branch: cmdline.extend(['--branch', branch]) sess = self.run(cmdline, validopts) sess.commandFinished.connect(self._onPushFinished) self.showMessage.emit(_('Pushing to %s...') % link) @pyqtSlot(int) def _onPushFinished(self, ret): link = self.linkifyWithTarget(self._lasturl) 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 ("'hg push --new-branch'" in self._cmdsession.errorString() 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']) sess = self.run(cmdline, ('force', 'new-branch', 'rev', 'bookmark', 'mq')) sess.commandFinished.connect(self._onPushFinished) return self.pushCompleted.emit() def postpullclicked(self): dlg = PostPullDialog(self._repoagent, self) dlg.setWindowFlags(Qt.Sheet) dlg.setWindowModality(Qt.WindowModal) dlg.exec_() def emailclicked(self): cmdline = ['outgoing', '--template', '{node}\n'] sess = self.run(cmdline, ('force', 'branch', 'rev')) sess.setCaptureOutput(True) sess.commandFinished.connect(self._onOutgoingEmailFinished) self.showMessage.emit(_('Determining outgoing changesets to email...')) def syncBookmark(self): dlg = bookmark.SyncBookmarkDialog(self._repoagent, self.currentUrl(), self) dlg.exec_() @pyqtSlot(int) def _onOutgoingEmailFinished(self, ret): if ret == 0: cmdline = self.lastcmdline data = str(self._cmdsession.readAll()) revs = tuple(self.repo[n].rev() for n in _extractnodeids(data)) 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) 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, _filter = 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 incoming command, which will show the revisions in # the bundle, and let the user accept or reject them self.inclicked() @pyqtSlot(str) def removeAlias(self, alias): alias = hglib.fromunicode(alias) fn = self.repo.vfs.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] try: wconfig.writefile(cfg, fn) self._repoagent.pollStatus() except EnvironmentError, e: qtlib.WarningMsgBox(_('Unable to write configuration file'), hglib.tounicode(str(e)), parent=self) self.reload() class PostPullDialog(QDialog): def __init__(self, repoagent, parent): super(PostPullDialog, self).__init__(parent) self._repoagent = repoagent repo = repoagent.rawRepo() 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._opchecks = { 'none': QRadioButton(_('None - simply pull changesets')), 'update': QRadioButton(_('Update - pull, then try to update')), } layout.addWidget(self._opchecks['none']) layout.addWidget(self._opchecks['update']) if 'fetch' in repo.extensions(): btntxt = _('Fetch - use fetch (auto merge pulled changes)') else: btntxt = _('Fetch - use fetch extension (fetch is not active!)') self._opchecks['fetch'] = chk = QRadioButton(btntxt) layout.addWidget(chk) chk.setVisible('fetch' in repo.extensions()) 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._opchecks['rebase'] = chk = QRadioButton(rebasetxt) layout.addWidget(chk) chk.setVisible('rebase' in repo.extensions()) self._opchecks['updateorrebase'] = chk = QRadioButton(updateorrebasetxt) layout.addWidget(chk) chk.setVisible('rebase' in repo.extensions()) chk = self._opchecks[repo.postpull] chk.setChecked(True) chk.show() self.autoresolve_chk = QCheckBox(_('Automatically resolve merge ' 'conflicts where possible')) self.autoresolve_chk.setChecked( repo.ui.configbool('tortoisehg', 'autoresolve', True)) 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) @property def repo(self): return self._repoagent.rawRepo() 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): return iter(op for op, chk in self._opchecks.iteritems() if chk.isChecked()).next() def accept(self): path = self.repo.vfs.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 try: cfg.set('tortoisehg', 'postpull', self.getValue()) cfg.set('tortoisehg', 'autoresolve', self.autoresolve_chk.isChecked()) wconfig.writefile(cfg, fn) self._repoagent.pollStatus() except EnvironmentError, e: qtlib.WarningMsgBox(_('Unable to write configuration file'), hglib.tounicode(str(e)), parent=self) super(PostPullDialog, self).accept() class SaveDialog(QDialog): def __init__(self, repoagent, alias, urlu, parent, edit=False): super(SaveDialog, self).__init__(parent) self._repoagent = repoagent self.setWindowTitle(_('Save Path')) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.origurl = hglib.fromunicode(urlu) self.setLayout(QFormLayout()) self.layout().setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) self.origalias = alias self.aliasentry = QLineEdit(hglib.tounicode(self.origalias)) self.aliasentry.selectAll() self.aliasentry.textChanged.connect(self._updateUi) self.layout().addRow(_('Alias'), self.aliasentry) self.edit = edit stack = QStackedLayout() # 0: read-only masked URL, 1: editable URL self.urllabel = QLabel(urlu) stack.addWidget(self.urllabel) self.urlentry = QLineEdit(urlu) self.urlentry.textChanged.connect(self._updateUi) stack.addWidget(self.urlentry) stack.setCurrentIndex(int(edit)) self.layout().addRow(_('URL'), stack) u = parseurl(urlu) clearable = bool(not edit and (u.user or u.passwd) and u.scheme in ('http', 'https')) 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.setChecked(clearable) self.clearcb.setVisible(clearable) self.clearcb.toggled.connect(self._removeAuthData) self.layout().addRow(self.clearcb) s = QSettings() self.updatesubpaths = QCheckBox(_('Update subrepo paths')) self.updatesubpaths.setChecked( qtlib.readBool(s, 'sync/updatesubpaths', True)) 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(bb) self._removeAuthData(self.clearcb.isChecked()) self._updateUi() def savePath(self, repo, alias, path, confirm=True): fn = repo.vfs.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()) path = hglib.fromunicode(self.urlentry.text()) repo = self._repoagent.rawRepo() self.savePath(repo, alias, path) self._repoagent.pollStatus() s = QSettings() s.setValue('sync/updatesubpaths', self.updatesubpaths.isChecked()) super(SaveDialog, self).accept() @pyqtSlot(bool) def _removeAuthData(self, showclean): if showclean: cleanurl = hglib.tounicode(util.removeauth(self.origurl)) self.urllabel.setText(cleanurl) self.urlentry.setText(cleanurl) else: safeurl = hglib.tounicode(util.hidepassword(self.origurl)) self.urllabel.setText(safeurl) self.urlentry.setText(hglib.tounicode(self.origurl)) @pyqtSlot() def _updateUi(self): savebtn = self.bb.button(QDialogButtonBox.Save) savebtn.setEnabled(bool(self.aliasentry.text() and self.urlentry.text())) def _addBrowseButton(edit, slot): button = QPushButton(_('Browse...')) button.setAutoDefault(False) button.clicked.connect(slot) hbox = QHBoxLayout() hbox.addWidget(edit) hbox.addWidget(button) return hbox class SecureDialog(QDialog): def __init__(self, repoagent, urlu, parent): super(SecureDialog, self).__init__(parent) self._repoagent = repoagent self._querysess = cmdcore.nullCmdSession() repo = repoagent.rawRepo() self._url = urlu 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.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('hostsecurity', u.host + ':fingerprints', '') if not fprint: fprint = repo.ui.config('hostfingerprints', u.host, '') if fprint: fprint = 'sha1:' + fprint 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) self._querybutton = qb = QPushButton(_('Query')) qb.clicked.connect(self._queryFingerprint) hbox.addWidget(qb) vbox.addWidget(self.cacertradio) vbox.addWidget(self.fprintradio) vbox.addLayout(hbox) vbox.addWidget(self.insecureradio) self.cacertradio.setChecked(True) # default if fprint: self.fprintradio.setChecked(True) elif repo.ui.config('insecurehosts', u.host): self.insecureradio.setChecked(True) self.fprintradio.toggled.connect(self._updateUi) self.insecureradio.toggled.connect(self._updateUi) self._protocolcombo = e = QComboBox(self) e.addItem(_(''), '') e.addItem(_('TLS 1.0'), 'tls1.0') e.addItem(_('TLS 1.1'), 'tls1.1') e.addItem(_('TLS 1.2'), 'tls1.2') protocol = repo.ui.config('hostsecurity', '%s:minimumprotocol' % u.host) e.setCurrentIndex(e.findData(hglib.tounicode(protocol or ''))) hbox = QHBoxLayout() hbox.addWidget(QLabel(_('Minimum Protocol'))) hbox.addWidget(self._protocolcombo) vbox.addLayout(hbox) self._authentries = {} # key: QLineEdit authbox = QGroupBox(_('User Authentication')) form = QFormLayout() authbox.setLayout(form) self.layout().addWidget(authbox) k = 'username' self._authentries[k] = e = QLineEdit(u.user or auth.get(k, '')) e.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'), e) k = 'password' self._authentries[k] = e = QLineEdit(u.passwd or auth.get(k, '')) e.setEchoMode(QLineEdit.Password) e.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'), e) if 'mercurial_keyring' in repo.extensions(): e.clear() e.setEnabled(False) e.setToolTip(_('Mercurial keyring extension is enabled. ' 'Passwords will be stored in a platform-native ' 'secure method.')) k = 'key' self._authentries[k] = e = QLineEdit(auth.get(k, '')) e.setToolTip( _('''Optional. PEM encoded client certificate key file. Environment variables are expanded in the filename.''')) form.addRow(_('User Certificate Key'), _addBrowseButton(e, self._browseClientKey)) k = 'cert' self._authentries[k] = e = QLineEdit(auth.get(k, '')) e.setToolTip( _('''Optional. PEM encoded client certificate chain file. Environment variables are expanded in the filename.''')) form.addRow(_('User Certificate Chain'), _addBrowseButton(e, self._browseClientCert)) 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._updateUi() e = self._authentries['username'] e.selectAll() QTimer.singleShot(0, e.setFocus) @pyqtSlot() def _queryFingerprint(self): cmdline = hglib.buildcmdargs('debuggethostfingerprint', self._url, insecure=True) self._querysess = sess = self._repoagent.runCommand(cmdline, self) sess.setCaptureOutput(True) sess.commandFinished.connect(self._onQueryFingerprintFinished) self._updateUi() @pyqtSlot(int) def _onQueryFingerprintFinished(self, ret): sess = self._querysess if ret == 0: data = str(sess.readAll()) self.fprintentry.setText(hglib.tounicode(data).strip()) else: cmdui.errorMessageBox(sess, self, _('Certificate Query Error')) self._updateUi() def keyringHelp(self): qtlib.openhelpcontents('sync.html#security') @pyqtSlot() def _browseClientKey(self): e = self._authentries['key'] n, _f = QFileDialog.getOpenFileName( self, _('Select User Certificate Key File'), e.text(), ';;'.join([_('PEM files (*.pem *.key)'), _('All files (*)')])) if n: e.setText(n) @pyqtSlot() def _browseClientCert(self): e = self._authentries['cert'] n, _f = QFileDialog.getOpenFileName( self, _('Select User Certificate Chain File'), e.text(), ';;'.join([_('PEM files (*.pem *.crt *.cer)'), _('All files (*)')])) if n: e.setText(n) def accept(self): path = hglib.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('hostsecurity', '%s:fingerprints' % self.host, fprint) setorclear('insecurehosts', self.host, insecure) e = self._protocolcombo protocol = hglib.fromunicode(e.itemData(e.currentIndex())) setorclear('hostsecurity', '%s:minimumprotocol' % self.host, protocol) cfg.set('auth', self.alias+'.prefix', self.host) for k in ['username', 'password', 'key', 'cert']: setorclear('auth', '%s.%s' % (self.alias, k), hglib.fromunicode(self._authentries[k].text())) setorclear('auth', self.alias+'.schemes', self.schemes) try: wconfig.writefile(cfg, fn) self._repoagent.pollStatus() except EnvironmentError, e: qtlib.WarningMsgBox(_('Unable to write configuration file'), hglib.tounicode(str(e)), parent=self) super(SecureDialog, self).accept() @pyqtSlot() def _updateUi(self): self._querybutton.setEnabled(self.fprintradio.isChecked() and self._querysess.isFinished()) self._protocolcombo.setEnabled(not self.insecureradio.isChecked()) class PathsTree(QTreeView): removeAlias = pyqtSignal(str) menuRequest = pyqtSignal(QPoint, str, str, 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) url = index.sibling(index.row(), 1).data(Qt.DisplayRole) 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) 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 None if role == Qt.DisplayRole: return self.rows[index.row()][index.column()] return None def headerData(self, col, orientation, role=Qt.DisplayRole): if role != Qt.DisplayRole or orientation != Qt.Horizontal: return None else: return 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, repoagent, opts, parent): QDialog.__init__(self, parent) self.setWindowTitle(_('%s - sync options') % repoagent.displayName()) 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) repo = repoagent.rawRepo() self.noproxycb = QCheckBox( _('Temporarily disable configured HTTP proxy')) self.noproxycb.setChecked(opts.get('noproxy', False)) layout.addWidget(self.noproxycb) proxy = 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) self.mqcb = QCheckBox(_('Work on patch queue (--mq)')) self.mqcb.setChecked(opts.get('mq', False)) self.mqcb.setVisible('mq' in repo.extensions()) 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.Save|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['force'] = self.forcecb.isChecked() outopts['new-branch'] = self.newbranchcb.isChecked() outopts['noproxy'] = self.noproxycb.isChecked() outopts['debug'] = self.debugcb.isChecked() if self.mqcb.isVisibleTo(self): outopts['mq'] = self.mqcb.isChecked() self.outopts = outopts QDialog.accept(self) tortoisehg-4.5.2/tortoisehg/hgqt/cmdcore.py0000644000175000017500000007376513205035322021660 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import signal import struct import sys import time from .qtcore import ( QBuffer, QIODevice, QObject, QProcess, QProcessEnvironment, QTimer, pyqtSignal, pyqtSlot, ) from ..util import ( hglib, paths, pipeui, ) from ..util.i18n import _ class ProgressMessage(tuple): __slots__ = () def __new__(cls, topic, pos, item='', unit='', total=None): return tuple.__new__(cls, (topic, pos, item, unit, total)) @property def topic(self): return self[0] # unicode @property def pos(self): return self[1] # int or None @property def item(self): return self[2] # unicode @property def unit(self): return self[3] # unicode @property def total(self): return self[4] # int or None def __repr__(self): names = ('topic', 'pos', 'item', 'unit', 'total') fields = ('%s=%r' % (n, v) for n, v in zip(names, self)) return '%s(%s)' % (self.__class__.__name__, ', '.join(fields)) class UiHandler(object): """Interface to handle user interaction of Mercurial commands""" NoInput = 0 TextInput = 1 PasswordInput = 2 ChoiceInput = 3 def __init__(self): self._dataout = None def setPrompt(self, text, mode, default=None): pass def getLineInput(self): # '' to use default; None to abort return '' def setDataOutputDevice(self, device): # QIODevice to write data output; None to disable capturing self._dataout = device def writeOutput(self, data, label): if not self._dataout or label.startswith('ui.') or ' ui.' in label: return -1 return self._dataout.write(data) def _createDefaultUiHandler(uiparent): if uiparent is None: return UiHandler() # this makes layering violation but is handy to create GUI handler by # default. nobody would want to write # uihandler = cmdui.InteractiveUiHandler(self) # cmdagent.runCommand(..., uihandler) # in place of # cmdagent.runCommand(..., self) from tortoisehg.hgqt import cmdui return cmdui.InteractiveUiHandler(uiparent) class _ProtocolError(Exception): """Error while processing server message; must be caught by CmdWorker""" class CmdWorker(QObject): """Back-end service to run Mercurial commands""" # If worker has permanent service, serviceState() should be overridden # to represent the availability of the service. NoService denotes that # it can run command or quit immediately. NoService = 0 Starting = 1 Ready = 2 Stopping = 3 Restarting = 4 NotRunning = 5 serviceStateChanged = pyqtSignal(int) commandFinished = pyqtSignal(int) outputReceived = pyqtSignal(str, str) progressReceived = pyqtSignal(ProgressMessage) def serviceState(self): return CmdWorker.NoService def startService(self): # NotRunning->Starting; Stopping->Restarting->Starting; *->* pass def stopService(self): # {Starting,Ready,Restarting}->Stopping; *->* pass def startCommand(self, cmdline, uihandler): raise NotImplementedError def abortCommand(self): raise NotImplementedError def isCommandRunning(self): raise NotImplementedError _localprocexts = [ 'tortoisehg.util.hgcommands', 'tortoisehg.util.partialcommit', 'tortoisehg.util.pipeui', ] _localserverexts = [ 'tortoisehg.util.hgdispatch', ] if os.name == 'nt': # to translate WM_CLOSE posted by QProcess.terminate() _localprocexts.append('tortoisehg.util.win32ill') def _interruptproc(proc): proc.terminate() else: def _interruptproc(proc): os.kill(int(proc.pid()), signal.SIGINT) def _fixprocenv(proc): env = QProcessEnvironment.systemEnvironment() # disable flags and extensions that might break our output parsing # (e.g. "defaults" arguments, "PAGER" of "email --test") env.insert('HGPLAINEXCEPT', 'alias,i18n,revsetalias') # since sys.path may contain the script directory which the hg process # wouldn't see, we have to filter it out libpaths = set(sys.path) libpaths.discard(os.path.dirname(os.path.realpath(sys.argv[0]))) thgroot = paths.get_prog_root() if not getattr(sys, 'frozen', False) and thgroot not in libpaths: # make sure hg process can look up our modules pypath = hglib.tounicode(thgroot) if env.contains('PYTHONPATH'): pypath += os.pathsep + unicode(env.value('PYTHONPATH')) env.insert('PYTHONPATH', pypath) proc.setProcessEnvironment(env) def _proccmdline(ui, exts): configs = [(section, name, value) for section, name, value in ui.walkconfig() if ui.configsource(section, name) == '--config'] configs.extend(('extensions', e, '') for e in exts) cmdline = list(paths.get_hg_command()) for section, name, value in configs: cmdline.extend(('--config', '%s.%s=%s' % (section, name, value))) return map(hglib.tounicode, cmdline) class CmdProc(CmdWorker): 'Run mercurial command in separate process' def __init__(self, ui, parent=None, cwd=None): super(CmdProc, self).__init__(parent) self._ui = ui self._uihandler = None self._proc = proc = QProcess(self) _fixprocenv(proc) if cwd: proc.setWorkingDirectory(cwd) proc.finished.connect(self._finish) proc.readyReadStandardOutput.connect(self._stdout) proc.readyReadStandardError.connect(self._stderr) proc.error.connect(self._handleerror) def startCommand(self, cmdline, uihandler): self._uihandler = uihandler fullcmdline = _proccmdline(self._ui, _localprocexts) fullcmdline.extend(cmdline) self._proc.start(fullcmdline[0], fullcmdline[1:], QIODevice.ReadOnly) def abortCommand(self): if not self.isCommandRunning(): return _interruptproc(self._proc) def isCommandRunning(self): return self._proc.state() != QProcess.NotRunning @pyqtSlot(int) def _finish(self, ret): self._uihandler = None self.commandFinished.emit(ret) @pyqtSlot(QProcess.ProcessError) def _handleerror(self, error): if error == QProcess.FailedToStart: self.outputReceived.emit(_('failed to start command\n'), 'ui.error') self._finish(-1) elif error != QProcess.Crashed: self.outputReceived.emit(_('error while running command\n'), 'ui.error') @pyqtSlot() def _stdout(self): data = self._proc.readAllStandardOutput().data() self._processRead(data, '') @pyqtSlot() def _stderr(self): data = self._proc.readAllStandardError().data() self._processRead(data, 'ui.error') def _processRead(self, fulldata, defaultlabel): for data in pipeui.splitmsgs(fulldata): msg, label = pipeui.unpackmsg(data) if (not defaultlabel # only stdout and self._uihandler.writeOutput(msg, label) >= 0): continue msg = hglib.tounicode(msg) label = hglib.tounicode(label) if 'ui.progress' in label.split(): progress = ProgressMessage(*pipeui.unpackprogress(msg)) self.progressReceived.emit(progress) else: self.outputReceived.emit(msg, label or defaultlabel) class CmdServer(CmdWorker): """Run Mercurial commands in command server process""" def __init__(self, ui, parent=None, cwd=None): super(CmdServer, self).__init__(parent) self._ui = ui self._uihandler = UiHandler() self._readchtable = self._idlechtable self._readq = [] # (ch, data or datasize), ... # deadline for arrival of hello message and immature data sec = ui.configint('tortoisehg', 'cmdserver.readtimeout', 30) self._readtimer = QTimer(self, interval=sec * 1000, singleShot=True) self._readtimer.timeout.connect(self._onReadTimeout) self._proc = self._createProc(cwd) self._servicestate = CmdWorker.NotRunning def _createProc(self, cwd): proc = QProcess(self) _fixprocenv(proc) if cwd: proc.setWorkingDirectory(cwd) proc.error.connect(self._onServiceError) proc.finished.connect(self._onServiceFinished) proc.setReadChannel(QProcess.StandardOutput) proc.readyRead.connect(self._onReadyRead) proc.readyReadStandardError.connect(self._onReadyReadError) return proc def serviceState(self): return self._servicestate def _changeServiceState(self, newstate): if self._servicestate == newstate: return self._servicestate = newstate self.serviceStateChanged.emit(newstate) def startService(self): if self._servicestate == CmdWorker.NotRunning: self._startService() elif self._servicestate == CmdWorker.Stopping: self._changeServiceState(CmdWorker.Restarting) def _startService(self): if self._proc.bytesToWrite() > 0: # QTBUG-44517: recreate QProcess to discard remainder of last # request; otherwise it would be written to new process oldproc = self._proc self._proc = self._createProc(oldproc.workingDirectory()) oldproc.setParent(None) cmdline = _proccmdline(self._ui, _localprocexts + _localserverexts) cmdline.extend(['serve', '--cmdserver', 'pipe', '--config', 'ui.interactive=True']) self._readchtable = self._hellochtable self._readtimer.start() self._changeServiceState(CmdWorker.Starting) self._proc.start(cmdline[0], cmdline[1:]) def stopService(self): if self._servicestate in (CmdWorker.Starting, CmdWorker.Ready): self._stopService() elif self._servicestate == CmdWorker.Restarting: self._changeServiceState(CmdWorker.Stopping) def _stopService(self): self._changeServiceState(CmdWorker.Stopping) _interruptproc(self._proc) # make sure "serve" loop ends by EOF (necessary on Windows) self._proc.closeWriteChannel() def _emitError(self, msg): self.outputReceived.emit('cmdserver: %s\n' % msg, 'ui.error') @pyqtSlot(QProcess.ProcessError) def _onServiceError(self, error): self._emitError(self._proc.errorString()) if error == QProcess.FailedToStart: self._onServiceFinished() @pyqtSlot() def _onServiceFinished(self): self._uihandler = UiHandler() self._readchtable = self._idlechtable del self._readq[:] self._readtimer.stop() if self._servicestate == CmdWorker.Restarting: self._startService() return if self._servicestate != CmdWorker.Stopping: self._emitError(_('process exited unexpectedly with code %d') % self._proc.exitCode()) self._changeServiceState(CmdWorker.NotRunning) def isCommandRunning(self): return self._readchtable is self._runcommandchtable def startCommand(self, cmdline, uihandler): assert self._servicestate == CmdWorker.Ready assert not self.isCommandRunning() try: data = hglib.fromunicode('\0'.join(cmdline)) except UnicodeEncodeError, inst: self._emitError(_('failed to encode command: %s') % inst) self._finishCommand(-1) return self._uihandler = uihandler self._readchtable = self._runcommandchtable self._proc.write('runcommand\n') self._writeBlock(data) def abortCommand(self): if not self.isCommandRunning(): return _interruptproc(self._proc) def _finishCommand(self, ret): self._uihandler = UiHandler() self._readchtable = self._idlechtable self.commandFinished.emit(ret) def _writeBlock(self, data): self._proc.write(struct.pack('>I', len(data))) self._proc.write(data) @pyqtSlot() def _onReadyRead(self): proc = self._proc headersize = 5 try: while True: header = str(proc.peek(headersize)) if not header: self._readtimer.stop() break if len(header) < headersize: self._readtimer.start() break ch, datasize = struct.unpack('>cI', header) if ch in 'IL': # input channel has no data proc.read(headersize) self._readq.append((ch, datasize)) continue if proc.bytesAvailable() < headersize + datasize: self._readtimer.start() break proc.read(headersize) data = str(proc.read(datasize)) self._readq.append((ch, data)) # don't do much things in readyRead slot for simplicity QTimer.singleShot(0, self._dispatchRead) except Exception: self.stopService() raise @pyqtSlot() def _onReadTimeout(self): startbytes = str(self._proc.peek(20)) if startbytes: # data corruption because bad extension might write to stdout? self._emitError(_('timed out while reading: %r...') % startbytes) else: self._emitError(_('timed out waiting for message')) self.stopService() @pyqtSlot() def _dispatchRead(self): try: while self._readq: ch, dataorsize = self._readq.pop(0) try: chfunc = self._readchtable[ch] except KeyError: if not ch.isupper(): continue raise _ProtocolError(_('unexpected response on required ' 'channel %r') % ch) chfunc(self, ch, dataorsize) except _ProtocolError, inst: self._emitError(inst.args[0]) self.stopService() except Exception: self.stopService() raise @pyqtSlot() def _onReadyReadError(self): fulldata = str(self._proc.readAllStandardError()) for data in pipeui.splitmsgs(fulldata): msg, label = pipeui.unpackmsg(data) msg = hglib.tounicode(msg) label = hglib.tounicode(label) self.outputReceived.emit(msg, label or 'ui.error') def _processHello(self, _ch, data): try: fields = dict(l.split(':', 1) for l in data.splitlines()) capabilities = fields['capabilities'].split() except (KeyError, ValueError): raise _ProtocolError(_('invalid "hello" message: %r') % data) if 'runcommand' not in capabilities: raise _ProtocolError(_('no "runcommand" capability')) self._readchtable = self._idlechtable self._changeServiceState(CmdWorker.Ready) def _processOutput(self, ch, data): msg, label = pipeui.unpackmsg(data) if ch == 'o' and self._uihandler.writeOutput(msg, label) >= 0: return msg = hglib.tounicode(msg) label = hglib.tounicode(label) labelset = label.split() if 'ui.progress' in labelset: progress = ProgressMessage(*pipeui.unpackprogress(msg)) self.progressReceived.emit(progress) elif 'ui.prompt' in labelset: if 'ui.getpass' in labelset: mode = UiHandler.PasswordInput elif 'ui.promptchoice' in labelset: mode = UiHandler.ChoiceInput else: mode = UiHandler.TextInput prompt, default = pipeui.unpackprompt(msg) self._uihandler.setPrompt(prompt, mode, default) else: self.outputReceived.emit(msg, label) def _processCommandResult(self, _ch, data): try: ret, = struct.unpack('>i', data) except struct.error: raise _ProtocolError(_('corrupted command result: %r') % data) self._finishCommand(ret) def _processLineRequest(self, _ch, size): text = self._uihandler.getLineInput() if text is None: self._writeBlock('') return try: data = hglib.fromunicode(text) + '\n' except UnicodeEncodeError, inst: self._emitError(_('failed to encode input: %s') % inst) self.abortCommand() return for start in xrange(0, len(data), size): self._writeBlock(data[start:start + size]) _idlechtable = { 'o': _processOutput, 'e': _processOutput, } _hellochtable = { 'o': _processHello, 'e': _processOutput, } _runcommandchtable = { 'o': _processOutput, 'e': _processOutput, 'r': _processCommandResult, # implement 'I' (data input) channel if necessary 'L': _processLineRequest, } _workertypes = { 'proc': CmdProc, 'server': CmdServer, } class CmdSession(QObject): """Run Mercurial commands in a background thread or process""" commandFinished = pyqtSignal(int) # in order to receive only notification messages of session state, use # "controlMessage"; otherwise use "outputReceived" controlMessage = pyqtSignal(str) outputReceived = pyqtSignal(str, str) progressReceived = pyqtSignal(ProgressMessage) readyRead = pyqtSignal() def __init__(self, cmdlines, uihandler, parent=None): super(CmdSession, self).__init__(parent) self._uihandler = uihandler self._worker = None self._queue = list(cmdlines) self._qnextp = 0 self._abortbyuser = False self._erroroutputs = [] self._warningoutputs = [] self._dataoutrbuf = QBuffer(self) self._exitcode = 0 if not cmdlines: # assumes null session is failure for convenience self._exitcode = -1 def run(self, worker): '''Execute Mercurial command''' if self._worker or self._qnextp >= len(self._queue): return self._connectWorker(worker) if worker.serviceState() in (CmdWorker.NoService, CmdWorker.Ready): self._runNext() def abort(self): '''Cancel running Mercurial command''' if self.isRunning(): self._worker.abortCommand() self._qnextp = len(self._queue) self._abortbyuser = True elif not self.isFinished(): self._abortbyuser = True # -1 instead of 255 for compatibility with CmdThread self._finish(-1) 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 self._qnextp >= len(self._queue) and not self.isRunning() def isRunning(self): """True if a command is running; False if finished or not started yet""" return bool(self._worker) and self._qnextp > 0 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 def setCaptureOutput(self, enabled): """If enabled, data outputs (without "ui.*" label) are queued and outputReceived signal is not emitted in that case. This is useful for receiving data to be parsed or copied to the clipboard. """ # pseudo FIFO between client "rbuf" and worker "wbuf"; not efficient # for large data since all outputs will be stored in memory if enabled: self._dataoutrbuf.open(QIODevice.ReadOnly | QIODevice.Truncate) dataoutwbuf = QBuffer(self._dataoutrbuf.buffer()) dataoutwbuf.bytesWritten.connect(self.readyRead) dataoutwbuf.open(QIODevice.WriteOnly) else: self._dataoutrbuf.close() dataoutwbuf = None self.setOutputDevice(dataoutwbuf) def setOutputDevice(self, device): """If set, data outputs will be sent to the specified device""" if self.isRunning(): raise RuntimeError('command already running') self._uihandler.setDataOutputDevice(device) def read(self, maxlen): """Read output if capturing enabled; ui messages are not included""" return self._dataoutrbuf.read(maxlen) def readAll(self): return self._dataoutrbuf.readAll() def readLine(self, maxlen=0): return self._dataoutrbuf.readLine(maxlen) def canReadLine(self): return self._dataoutrbuf.canReadLine() def peek(self, maxlen): return self._dataoutrbuf.peek(maxlen) def _connectWorker(self, worker): self._worker = worker worker.serviceStateChanged.connect(self._onWorkerStateChanged) worker.commandFinished.connect(self._onCommandFinished) worker.outputReceived.connect(self.outputReceived) worker.outputReceived.connect(self._captureOutput) worker.progressReceived.connect(self.progressReceived) def _disconnectWorker(self): worker = self._worker if not worker: return worker.serviceStateChanged.disconnect(self._onWorkerStateChanged) worker.commandFinished.disconnect(self._onCommandFinished) worker.outputReceived.disconnect(self.outputReceived) worker.outputReceived.disconnect(self._captureOutput) worker.progressReceived.disconnect(self.progressReceived) self._worker = None def _emitControlMessage(self, msg): self.controlMessage.emit(msg) self.outputReceived.emit(msg + '\n', 'control') def _runNext(self): cmdline = self._queue[self._qnextp] self._qnextp += 1 self._emitControlMessage('% hg ' + hglib.prettifycmdline(cmdline)) self._worker.startCommand(cmdline, self._uihandler) def _finish(self, ret): self._qnextp = len(self._queue) self._disconnectWorker() self._exitcode = ret self.commandFinished.emit(ret) @pyqtSlot(int) def _onWorkerStateChanged(self, state): if state == CmdWorker.Ready: assert self._qnextp == 0 self._runNext() elif state == CmdWorker.NotRunning: # unexpected end of command execution self._finish(-1) @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._emitControlMessage(msg % time.asctime()) if ret != 0 or self._qnextp >= len(self._queue): self._finish(ret) else: self._runNext() @pyqtSlot(str, str) 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 exitCode() is -1 so that the command dialog can finish with error status if nothing executed. >>> sess = nullCmdSession() >>> sess.isFinished(), sess.isRunning(), sess.isAborted(), sess.exitCode() (True, False, False, -1) >>> sess.abort() # should not change flags >>> sess.isFinished(), sess.isRunning(), sess.isAborted(), sess.exitCode() (True, False, False, -1) Null session can be set up just like one made by runCommand(). It can be used as an object representing failure or canceled operation. >>> sess.setOutputDevice(QBuffer()) """ return CmdSession([], UiHandler()) class CmdAgent(QObject): """Manage requests of Mercurial commands""" serviceStopped = pyqtSignal() busyChanged = pyqtSignal(bool) # Signal forwarding: # worker ---- agent commandFinished: session (= last one of worker) # \ / outputReceived: worker + session # session progressReceived: worker (= session) # # Inactive session is not started by the agent, so agent.commandFinished # won't be emitted when waiting session is aborted. commandFinished = pyqtSignal(CmdSession) outputReceived = pyqtSignal(str, str) progressReceived = pyqtSignal(ProgressMessage) # isBusy() is False when the last commandFinished is emitted, but you # shouldn't rely on the emission order of busyChanged and commandFinished. def __init__(self, ui, parent=None, cwd=None, worker=None): super(CmdAgent, self).__init__(parent) self._ui = ui self._worker = self._createWorker(cwd, worker or 'server') self._sessqueue = [] # [active, waiting...] self._runlater = QTimer(self, interval=0, singleShot=True) self._runlater.timeout.connect(self._runNextSession) def isServiceRunning(self): stoppedstates = (CmdWorker.NoService, CmdWorker.NotRunning) return self._worker.serviceState() not in stoppedstates def stopService(self): """Shut down back-end services so that this can be deleted safely or reconfigured; serviceStopped will be emitted asynchronously""" self._worker.stopService() @pyqtSlot() def _tryEmitServiceStopped(self): if not self.isServiceRunning(): self.serviceStopped.emit() 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): del self._sessqueue[0] 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 _cleanupWaitingSession(self): for i in reversed(xrange(1, len(self._sessqueue))): sess = self._sessqueue[i] if sess.isFinished(): del self._sessqueue[i] sess.setParent(None) def runCommand(self, cmdline, uihandler=None): """Executes a single Mercurial command asynchronously and returns new CmdSession object""" return self.runCommandSequence([cmdline], uihandler) def runCommandSequence(self, cmdlines, uihandler=None): """Executes a series of Mercurial commands asynchronously and returns new CmdSession object which will provide notification signals. The optional uihandler is the call-back of user-interaction requests. If uihandler does not implement UiHandler interface, it will be used as the parent widget of the default InteractiveUiHandler. If uihandler is None, no interactive prompt will be displayed. If the specified uihandler is a UiHandler object, it should be created per request in order to avoid sharing the same uihandler across several CmdSession objects. CmdSession object will be disowned on command finished. The specified uihandler is unrelated to the lifetime of CmdSession object. If one of the preceding command exits with non-zero status, the following commands won't be executed. """ if not isinstance(uihandler, UiHandler): uihandler = _createDefaultUiHandler(uihandler) sess = CmdSession(cmdlines, uihandler, self) sess.commandFinished.connect(self._onCommandFinished) sess.controlMessage.connect(self._forwardControlMessage) self._enqueueSession(sess) return sess def abortCommands(self): """Abort running and queued commands; all command sessions will emit commandFinished""" for sess in self._sessqueue[:]: sess.abort() def _createWorker(self, cwd, name): self._ui.debug("creating cmdworker '%s'\n" % name) worker = _workertypes[name](self._ui, self, cwd) worker.serviceStateChanged.connect(self._tryEmitServiceStopped) worker.outputReceived.connect(self.outputReceived) worker.progressReceived.connect(self.progressReceived) return worker @pyqtSlot() def _runNextSession(self): sess = self._sessqueue[0] worker = self._worker assert not worker.isCommandRunning() sess.run(worker) # start after connected to sess so that it can receive immediate error worker.startService() @pyqtSlot() def _onCommandFinished(self): sess = self._sessqueue[0] if not sess.isFinished(): # waiting session is aborted, just delete it self._cleanupWaitingSession() return self._dequeueSession() self.commandFinished.emit(sess) sess.setParent(None) @pyqtSlot(str) def _forwardControlMessage(self, msg): self.outputReceived.emit(msg + '\n', 'control') tortoisehg-4.5.2/tortoisehg/hgqt/serve_ui.py0000644000175000017500000001101513242104460022042 0ustar sborhosborho00000000000000# -*- coding: utf-8 -*- # Form implementation generated from reading ui file '/home/sborho/repos/thg/tortoisehg/hgqt/serve.ui' # # Created by: PyQt5 UI code generator 5.5.1 # # WARNING! All changes made in this file will be lost! from tortoisehg.util.i18n import _ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_ServeDialog(object): def setupUi(self, ServeDialog): ServeDialog.setObjectName("ServeDialog") ServeDialog.resize(500, 400) self.dialog_layout = QtWidgets.QVBoxLayout(ServeDialog) self.dialog_layout.setObjectName("dialog_layout") self.top_layout = QtWidgets.QHBoxLayout() self.top_layout.setObjectName("top_layout") self.opts_layout = QtWidgets.QFormLayout() self.opts_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow) self.opts_layout.setObjectName("opts_layout") self.port_label = QtWidgets.QLabel(ServeDialog) self.port_label.setObjectName("port_label") self.opts_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.port_label) self.port_edit = QtWidgets.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, QtWidgets.QFormLayout.FieldRole, self.port_edit) self.status_label = QtWidgets.QLabel(ServeDialog) self.status_label.setObjectName("status_label") self.opts_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.status_label) self.status_edit = QtWidgets.QLabel(ServeDialog) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.status_edit.sizePolicy().hasHeightForWidth()) self.status_edit.setSizePolicy(sizePolicy) self.status_edit.setText("") self.status_edit.setTextFormat(QtCore.Qt.RichText) self.status_edit.setOpenExternalLinks(True) self.status_edit.setObjectName("status_edit") self.opts_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.status_edit) self.top_layout.addLayout(self.opts_layout) self.actions_layout = QtWidgets.QVBoxLayout() self.actions_layout.setObjectName("actions_layout") self.start_button = QtWidgets.QPushButton(ServeDialog) self.start_button.setDefault(True) self.start_button.setObjectName("start_button") self.actions_layout.addWidget(self.start_button) self.stop_button = QtWidgets.QPushButton(ServeDialog) self.stop_button.setAutoDefault(False) self.stop_button.setObjectName("stop_button") self.actions_layout.addWidget(self.stop_button) spacerItem = QtWidgets.QSpacerItem(0, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.actions_layout.addItem(spacerItem) self.settings_button = QtWidgets.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.top_layout.setStretch(0, 1) self.dialog_layout.addLayout(self.top_layout) self.details_tabs = QtWidgets.QTabWidget(ServeDialog) self.details_tabs.setObjectName("details_tabs") self.dialog_layout.addWidget(self.details_tabs) self.dialog_layout.setStretch(1, 1) 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): _translate = QtCore.QCoreApplication.translate 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-4.5.2/tortoisehg/hgqt/p4pending.py0000644000175000017500000001124113150123225022110 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QComboBox, QDialog, QDialogButtonBox, QVBoxLayout, ) from mercurial import error from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, cslist, ) class PerforcePending(QDialog): 'Dialog for selecting a revision' showMessage = pyqtSignal(str) 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[str].connect(self.p4clActivated) for changelist in self.pending: clcombo.addItem(hglib.tounicode(changelist)) self.p4clActivated(clcombo.currentText()) self.setWindowTitle(_('Pending Perforce Changelists - %s') % repoagent.displayName()) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) @pyqtSlot(str) 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) 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) 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-4.5.2/tortoisehg/hgqt/visdiff.py0000644000175000017500000005334613214542271021675 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import re import stat import subprocess import threading from .qtcore import ( QTimer, pyqtSlot, ) from .qtgui import ( QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QKeySequence, QLabel, QListWidget, QMessageBox, QShortcut, QVBoxLayout, ) from mercurial import ( copies, error, match, scmutil, util, ) from ..util import hglib from ..util.i18n import _ from . import qtlib # 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) fctx = ctx[wfn] data = repo.wwritedata(wfn, fctx.data()) f = open(dest, 'wb') f.write(data) f.close() if 'x' in fctx.flags(): util.setflags(dest, False, True) 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(): try: p = ui.configint('merge-tools', t + '.priority', 0) except error.ConfigError, inst: ui.warn('visdiff: %s\n' % inst) p = 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]' repoagent = repo._pyqtobj # TODO replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b, plabel1=label1a, plabel2=label1b, phash1=str(ctx1a), phash2=str(ctx1b), repo=hglib.fromunicode(repoagent.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) repoagent = repo._pyqtobj # TODO self.reponame = hglib.fromunicode(repoagent.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[str].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 = '%s %s' % (status, hglib.tounicode(f)) self.list.addItem(row) @pyqtSlot(str) 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): # hg>=4.4: configbool() may return None as the default is set to None 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(bool(d2)) self.p2button.setEnabled(bool(d2)) self.p3button.setEnabled(bool(d3)) elif hasattr(self, 'dbutton'): d2 = self.repo.ui.configbool('merge-tools', tool + '.dirdiff') self.dbutton.setEnabled(bool(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-4.5.2/tortoisehg/hgqt/hginit.py0000644000175000017500000001131413150123225021503 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os from .qtcore import ( pyqtSignal, pyqtSlot, ) from .qtgui import ( QCheckBox, QFileDialog, QFormLayout, QHBoxLayout, QLineEdit, QPushButton, QSizePolicy, QVBoxLayout, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, qtlib, ) class InitWidget(cmdui.AbstractCmdWidget): def __init__(self, ui, cmdagent, destdir='.', parent=None): super(InitWidget, self).__init__(parent) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self._cmdagent = cmdagent form = QFormLayout() self.setLayout(form) # dest widgets self._dest_edit = QLineEdit() self._dest_edit.setMinimumWidth(300) self._dest_btn = QPushButton(_('Browse...')) self._dest_btn.setAutoDefault(False) destbox = QHBoxLayout() destbox.addWidget(self._dest_edit, 1) destbox.addWidget(self._dest_btn) form.addRow(_('Destination path:'), destbox) # options checkboxes if ui.config('tortoisehg', 'initskel'): l = _('Copy working directory files from skeleton') else: l = _('Create special files (.hgignore, ...)') self._add_files_chk = QCheckBox(l) self._make_pre_1_7_chk = QCheckBox( _('Make repo compatible with Mercurial <1.7')) optbox = QVBoxLayout() optbox.addWidget(self._add_files_chk) optbox.addWidget(self._make_pre_1_7_chk) form.addRow('', optbox) # some extras self._hgcmd_txt = QLineEdit() self._hgcmd_txt.setReadOnly(True) form.addRow(_('Hg command:'), self._hgcmd_txt) # init defaults path = os.path.abspath(destdir) if os.path.isfile(path): path = os.path.dirname(path) self._dest_edit.setText(path) self._add_files_chk.setChecked(True) self._make_pre_1_7_chk.setChecked(False) self._composeCommand() # connecting slots self._dest_edit.textChanged.connect(self._composeCommand) self._dest_btn.clicked.connect(self._browseDestination) self._add_files_chk.toggled.connect(self._composeCommand) self._make_pre_1_7_chk.toggled.connect(self._composeCommand) @pyqtSlot() def _browseDestination(self): """Select the destination directory""" caption = _('Select Destination Folder') path = QFileDialog.getExistingDirectory(self, caption) if path: self._dest_edit.setText(path) def destination(self): return unicode(self._dest_edit.text()).strip() def _buildCommand(self): cfgs = [] if self._add_files_chk.isChecked(): cfgs.append('hooks.post-init.thgskel=' 'python:tortoisehg.util.hgcommands.postinitskel') if self._make_pre_1_7_chk.isChecked(): cfgs.append('format.dotencode=False') return hglib.buildcmdargs('init', self.destination(), config=cfgs) @pyqtSlot() def _composeCommand(self): cmdline = self._buildCommand() self._hgcmd_txt.setText('hg ' + hglib.prettifycmdline(cmdline)) self.commandChanged.emit() def canRunCommand(self): return bool(self.destination()) def runCommand(self): cmdline = self._buildCommand() return self._cmdagent.runCommand(cmdline, self) class InitDialog(cmdui.CmdControlDialog): newRepository = pyqtSignal(str) def __init__(self, ui, destdir='.', parent=None): super(InitDialog, self).__init__(parent) self.setWindowTitle(_('New Repository')) self.setWindowIcon(qtlib.geticon('hg-init')) self.setObjectName('init') self.setRunButtonText(_('&Create')) self._cmdagent = cmdagent = cmdcore.CmdAgent(ui, self) cmdagent.serviceStopped.connect(self.reject) self.setCommandWidget(InitWidget(ui, cmdagent, destdir, self)) self.commandFinished.connect(self._handleNewRepo) def destination(self): return self.commandWidget().destination() @pyqtSlot(int) def _handleNewRepo(self, ret): if ret != 0: return self.newRepository.emit(self.destination()) def done(self, r): if self._cmdagent.isServiceRunning(): self._cmdagent.stopService() return # postponed until serviceStopped super(InitDialog, self).done(r) tortoisehg-4.5.2/tortoisehg/hgqt/update.py0000644000175000017500000004143313153775104021524 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( pyqtSlot, ) from .qtgui import ( QCheckBox, QComboBox, QFormLayout, QLabel, QMessageBox, QSizePolicy, QVBoxLayout, ) from mercurial import error from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, csinfo, qtlib, resolve, ) class UpdateWidget(cmdui.AbstractCmdWidget): def __init__(self, repoagent, rev=None, parent=None, opts={}): super(UpdateWidget, self).__init__(parent) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self._repoagent = repoagent repo = repoagent.rawRepo() ## main layout form = QFormLayout() form.setContentsMargins(0, 0, 0, 0) form.setSpacing(6) self.setLayout(form) ### target revision combo self.rev_combo = combo = QComboBox() combo.setEditable(True) combo.setMinimumContentsLength(30) # cut long name combo.installEventFilter(qtlib.BadCompletionBlocker(combo)) form.addRow(_('Update to:'), combo) # always include integer revision try: assert not isinstance(rev, unicode) ctx = self.repo[rev] if isinstance(ctx.rev(), int): # could be None or patch name combo.addItem(str(ctx.rev())) except error.RepoLookupError: pass combo.addItems(map(hglib.tounicode, hglib.namedbranches(repo))) tags = list(self.repo.tags()) + repo._bookmarks.keys() tags.sort(reverse=True) combo.addItems(map(hglib.tounicode, tags)) 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() form.addRow(_('Target:'), self.target_info) ### parent revision info self.ctxs = self.repo[None].parents() if len(self.ctxs) == 2: self.p1_info = factory() form.addRow(_('Parent 1:'), self.p1_info) self.p2_info = factory() form.addRow(_('Parent 2:'), self.p2_info) else: self.p1_info = factory() form.addRow(_('Parent:'), self.p1_info) # show a subrepo "pull path" combo, with the # default path as the first (and default) path self.path_combo_label = QLabel(_('Pull subrepos from:')) self.path_combo = QComboBox(self) syncpaths = dict(repo.ui.configitems('paths')) aliases = sorted(syncpaths) # make sure that the default path is the first one if 'default' in aliases: aliases.remove('default') aliases.insert(0, 'default') for n, alias in enumerate(aliases): self.path_combo.addItem(hglib.tounicode(alias)) self.path_combo.setItemData( n, hglib.tounicode(syncpaths[alias])) self.path_combo.currentIndexChanged.connect( self._updatePathComboTooltip) self._updatePathComboTooltip(0) form.addRow(self.path_combo_label, self.path_combo) ### options self.optbox = QVBoxLayout() self.optbox.setSpacing(6) self.optexpander = expander = qtlib.ExpanderLabel(_('Options:'), False) expander.expanded.connect(self.show_options) form.addRow(expander, self.optbox) 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.optbox.addWidget(self.verbose_chk) self.optbox.addWidget(self.discard_chk) self.optbox.addWidget(self.merge_chk) self.optbox.addWidget(self.autoresolve_chk) self.discard_chk.setChecked(bool(opts.get('clean'))) # signal handlers self.rev_combo.editTextChanged.connect(self.update_info) self.discard_chk.toggled.connect(self.update_info) # prepare to show self.merge_chk.setHidden(True) self.autoresolve_chk.setHidden(True) self.update_info() if not self.canRunCommand(): # need to change rev self.rev_combo.lineEdit().selectAll() def readSettings(self, qs): self.merge_chk.setChecked(qtlib.readBool(qs, 'merge')) self.autoresolve_chk.setChecked( self.repo.ui.configbool('tortoisehg', 'autoresolve', qtlib.readBool(qs, 'autoresolve', True))) self.verbose_chk.setChecked(qtlib.readBool(qs, 'verbose')) # expand options if a hidden one is checked self.optexpander.set_expanded(self.hiddenSettingIsChecked()) def writeSettings(self, qs): qs.setValue('merge', self.merge_chk.isChecked()) qs.setValue('autoresolve', self.autoresolve_chk.isChecked()) qs.setValue('verbose', self.verbose_chk.isChecked()) @property def repo(self): return self._repoagent.rawRepo() def hiddenSettingIsChecked(self): return (self.merge_chk.isChecked() or self.autoresolve_chk.isChecked()) @pyqtSlot() def update_info(self): 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.commandChanged.emit() 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)')) else: self.target_info.update(self.repo[new_rev]) # only show the path combo when there are multiple paths # and the target revision has subrepos showpathcombo = self.path_combo.count() > 1 and \ '.hgsubstate' in new_ctx self.path_combo_label.setVisible(showpathcombo) self.path_combo.setVisible(showpathcombo) except (error.LookupError, error.RepoError, EnvironmentError): self.target_info.setText(_('unknown revision!')) self.commandChanged.emit() def canRunCommand(self): new_rev = hglib.fromunicode(self.rev_combo.currentText()) try: new_ctx = self.repo[new_rev] except (error.LookupError, error.RepoError, EnvironmentError): return False return (self.discard_chk.isChecked() or len(self.ctxs) == 2 or new_ctx.rev() != self.ctxs[0].rev() or bool(new_ctx.bookmarks())) def runCommand(self): cmdline = ['update'] 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), hglib.tounicode(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, hglib.activebookmark(self.repo)).run() if selectedbookmark: rev = selectedbookmark elif self.repo[rev] == self.repo[hglib.activebookmark(self.repo)]: deactivatebookmark = qtlib.QuestionMsgBox( _('Deactivate current bookmark?'), _('Do you really want to deactivate the %s ' 'bookmark?') % hglib.tounicode(hglib.activebookmark(self.repo))) if deactivatebookmark: cmdline = ['bookmark'] if self.verbose_chk.isChecked(): cmdline += ['--verbose'] cmdline += ['-i', hglib.tounicode(hglib.activebookmark(self.repo))] return self._repoagent.runCommand(cmdline, self) return cmdcore.nullCmdSession() cmdline.append('--rev') cmdline.append(rev) pullpathname = hglib.fromunicode( self.path_combo.currentText()) if pullpathname and pullpathname != 'default': # We must tell mercurial to pull any missing repository # revisions from the selected path. The only way to do so is # to temporarily set the default path to the selected path URL pullpath = hglib.fromunicode( self.path_combo.itemData(self.path_combo.currentIndex())) cmdline.append('--config') cmdline.append('paths.default=%s' % pullpath) if self.discard_chk.isChecked(): cmdline.append('--clean') else: cur = self.repo.hgchangectx('.') try: node = self.repo.hgchangectx(rev) except (error.LookupError, error.RepoError, EnvironmentError): return cmdcore.nullCmdSession() 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 cmdcore.nullCmdSession() elif clicked == 'merge': pass # no args else: return cmdcore.nullCmdSession() cmdline = map(hglib.tounicode, cmdline) return self._repoagent.runCommand(cmdline, self) @pyqtSlot(bool) def show_options(self, visible): self.merge_chk.setVisible(visible) self.autoresolve_chk.setVisible(visible) @pyqtSlot(int) def _updatePathComboTooltip(self, idx): self.path_combo.setToolTip(self.path_combo.itemData(idx)) class UpdateDialog(cmdui.CmdControlDialog): def __init__(self, repoagent, rev=None, parent=None, opts={}): super(UpdateDialog, self).__init__(parent) self._repoagent = repoagent self.setWindowTitle(_('Update - %s') % repoagent.displayName()) self.setWindowIcon(qtlib.geticon('hg-update')) self.setObjectName('update') self.setRunButtonText(_('&Update')) self.setCommandWidget(UpdateWidget(repoagent, rev, self, opts)) self.commandFinished.connect(self._checkMergeConflicts) @pyqtSlot(int) def _checkMergeConflicts(self, ret): if ret != 1: return qtlib.InfoMsgBox(_('Merge caused file conflicts'), _('File conflicts need to be resolved')) dlg = resolve.ResolveDialog(self._repoagent, self) dlg.exec_() if not self.isLogVisible(): self.reject() tortoisehg-4.5.2/tortoisehg/hgqt/run.py0000644000175000017500000013010613242076403021035 0ustar sborhosborho00000000000000# 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-2018 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 getopt import os import pdb import sys import subprocess from mercurial import util, fancyopts, cmdutil, extensions, error, scmutil from mercurial import pathutil, registrar from tortoisehg.util.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 cmdui, 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 debugblockmatcher 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 = hglib.loadui() if '--traceback' in args: u.setconfig('ui', 'traceback', 'on') if '--debugger' in args: pdb.set_trace() return _runcatch(u, args) except error.ParseError, e: qtapp.earlyExceptionMsgBox(hglib.tounicode(str(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 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 'THG_OSX_APP' in os.environ: # guifork seems to break Mac app bundles return elif ui.configbool('tortoisehg', 'guifork', None) is not None: if not ui.configbool('tortoisehg', 'guifork'): return elif config_nofork: return os.environ['THG_GUI_SPAWN'] = '1' try: _forkbg(ui) except OSError, inst: ui.warn(_('failed to fork GUI process: %s\n') % inst.strerror) # native window API can't be used after fork() on Mac OS X if os.name == 'posix' and sys.platform != 'darwin': def _forkbg(ui): pid = os.fork() if pid > 0: sys.exit(0) # disables interaction with tty, keeping logs sent to stdout/stderr nullfd = os.open(os.devnull, os.O_RDONLY) os.dup2(nullfd, ui.fin.fileno()) os.close(nullfd) else: _origwdir = os.getcwd() def _forkbg(ui): # Spawn background process and exit cmdline = list(paths.get_thg_command()) cmdline.extend(sys.argv[1:]) os.chdir(_origwdir) # redirect stdout/stderr to "nul" device; otherwise EBADF could occur # on cmd.exe (#4837). for stdin, create a pipe since "nul" is a tty # on Windows! (#4469) nullfd = os.open(os.devnull, os.O_RDWR) p = subprocess.Popen(cmdline, stdin=subprocess.PIPE, stdout=nullfd, stderr=nullfd, creationflags=qtlib.openflags) p.stdin.close() os.close(nullfd) sys.exit(0) # 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 = pathutil.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 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 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: # read --config before doing anything else like Mercurial hglib.parseconfigopts(ui, args) # register config items specific to TortoiseHg GUI ui.setconfig('extensions', 'tortoisehg.util.configitems', '', 'run') 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) except util.Abort, inst: ui.warn(_("abort: %s\n") % inst) if inst.hint: ui.warn(_("(%s)\n") % inst.hint) return -1 def runcommand(ui, args): cmd, func, args, options, cmdoptions, alias = _parse(ui, args) if options['config']: raise util.Abort(_('option --config may not be abbreviated!')) cmdoptions['alias'] = alias ui.setconfig("ui", "verbose", str(bool(options["verbose"]))) ui.setconfig('ui', 'debug', str(bool(options['debug'] or 'THGDEBUG' in os.environ))) i18n.setlanguage(ui.config('tortoisehg', 'ui.language')) if options['help']: return help_(ui, cmd) 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.loadextensions(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: # disables interaction with tty as it would block GUI events ui.setconfig('ui', 'interactive', 'off', 'qtrun') ui.setconfig('ui', 'paginate', 'off', 'qtrun') # ANSI color output is useless in GUI ui.setconfig('ui', 'color', 'off', 'qtrun') 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 = registrar.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')), ('', 'config', [], _("set/override config option (use 'section.name=value')")), ('', 'debug', None, _('enable debugging output')), ('', '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')), ] # common command functions def _formatfilerevset(pats): q = ["file('path:%s')" % f for f in hglib.canonpaths(pats)] 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) rev = opts.get('rev') if rev: w.goto(hglib.fromunicode(root), rev) q = opts.get('query') or _formatfilerevset(pats) if q: w.setRevsetFilter(root, hglib.tounicode(q)) if not w.currentRepoRootPath(): 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 dlg = filelog(ui, repoagent, *pats, **opts) dlg.setFileViewMode(fileview.AnnMode) 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.createArchiveDialog(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' repo = repoagent.rawRepo() rev = scmutil.revsingle(repo, rev).rev() msg = backoutmod.checkrev(repo, rev) if msg: raise util.Abort(hglib.fromunicode(msg)) 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) @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 dlg = clonemod.CloneDialog(ui, pats, opts) dlg.clonedRepository.connect(qtrun.openRepoInWorkbench) return dlg @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('debugblockmatcher', [], _('thg debugblockmatcher')) def debugblockmatcher(ui, *pats, **opts): """show blockmatcher widget""" from tortoisehg.hgqt import blockmatcher return blockmatcher.createTestWidget(ui) @command('debugbugreport', [], _('thg debugbugreport [TEXT]')) def debugbugreport(ui, *pats, **opts): """open bugreport dialog by exception""" raise Exception(' '.join(pats)) @command('debugconsole', [], _('thg debugconsole')) def debugconsole(ui, repoagent, *pats, **opts): """open console window""" from tortoisehg.hgqt import docklog dlg = docklog.ConsoleWidget(repoagent) dlg.closeRequested.connect(dlg.close) dlg.resize(700, 400) return dlg @command('debuglighthg', [], _('thg debuglighthg')) def debuglighthg(ui, repoagent, *pats, **opts): from tortoisehg.hgqt import repowidget return repowidget.LightRepoWindow(repoagent) @command('debugruncommand', [], _('thg debugruncommand -- COMMAND [ARGUMENT]...')) def debugruncommand(ui, repoagent, *cmdline, **opts): """run hg command in dialog""" if not cmdline: raise util.Abort(_('no command specified')) dlg = cmdui.CmdSessionDialog() dlg.setLogVisible(ui.verbose) sess = repoagent.runCommand(map(hglib.tounicode, cmdline), dlg) dlg.setSession(sess) return dlg @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('^filelog', [('r', 'rev', '', _('select the specified revision')), ('', 'compare', False, _('side-by-side comparison of revisions'))], _('thg filelog [OPTION]... FILE')) def filelog(ui, repoagent, *pats, **opts): """show history of the specified file""" from tortoisehg.hgqt import filedialogs if len(pats) != 1: raise util.Abort(_('requires a single filename')) repo = repoagent.rawRepo() rev = scmutil.revsingle(repo, opts.get('rev')).rev() filename = hglib.canonpaths(pats)[0] if opts.get('compare'): dlg = filedialogs.FileDiffDialog(repoagent, filename) else: dlg = filedialogs.FileLogDialog(repoagent, filename) dlg.goto(rev) return dlg @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.vfs.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, dest='.', **opts): """init dialog""" from tortoisehg.hgqt import hginit dlg = hginit.InitDialog(ui, hglib.tounicode(dest)) dlg.newRepository.connect(qtrun.openRepoInWorkbench) return dlg @command('^lock|unlock', [], _('thg lock')) def lock(ui, repoagent, **opts): """lock dialog""" from tortoisehg.hgqt import locktool return locktool.LockDialog(repoagent) @command('^log|history|explorer|workbench', [('k', 'query', '', _('search for a given text or revset')), ('r', 'rev', '', _('select the specified revision')), ('l', 'limit', '', _('(DEPRECATED)')), ('', 'newworkbench', None, _('open a new workbench window'))], _('thg log [OPTIONS] [FILE]')) def log(ui, *pats, **opts): """workbench application""" if opts.get('query') and pats: # 'filelog' does not support -k, and multiple filenames are packed # into revset query that may conflict with user-supplied one. raise util.Abort(_('cannot specify both -k/--query and filenames')) 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: # TODO: send -rREV to server q = opts.get('query') or _formatfilerevset(pats) if qtapp.connectToExistingWorkbench(root, q): # 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 revdetails as revdetailsmod repo = repoagent.rawRepo() rev = scmutil.revsingle(repo, opts.get('rev')).rev() dlg = revdetailsmod.createManifestDialog(repoagent, rev) if pats: path = hglib.canonpaths(pats)[0] dlg.setFilePath(hglib.tounicode(path)) 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('^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')) repo = repoagent.rawRepo() rev = scmutil.revsingle(repo, rev).rev() 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() if 'reviewboard' not in repo.extensions(): url = 'https://www.mercurial-scm.org/wiki/ReviewboardExtension' raise util.Abort(_('reviewboard extension not enabled'), hint=(_('see %(url)s') % {'url': url})) 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('^prune|obsolete|kill', [('r', 'rev', [], _('revisions to prune'))], _('thg prune [-r] REV...')) def prune(ui, repoagent, *revs, **opts): """hide changesets by marking them obsolete""" from tortoisehg.hgqt import prune as prunemod revs = list(revs) revs.extend(opts.get('rev')) if len(revs) < 2: revspec = ''.join(revs) else: revspec = hglib.formatrevspec('%lr', revs) return prunemod.createPruneDialog(repoagent, hglib.tounicode(revspec)) @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.vfs.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, source=None, dest=None, **opts): """rename dialog""" from tortoisehg.hgqt import rename as renamemod repo = repoagent.rawRepo() cwd = repo.getcwd() if source: source = hglib.tounicode(pathutil.canonpath(repo.root, cwd, source)) if dest: dest = hglib.tounicode(pathutil.canonpath(repo.root, cwd, dest)) iscopy = (opts.get('alias') == 'copy') return renamemod.RenameDialog(repoagent, None, source, dest, 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.createRemoteUpdateDialog(repoagent, rev) @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')), ('k', 'keep', None, _('do not modify working copy during strip')), ('r', 'rev', '', _('revision to strip'))], _('thg strip [-k] [-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.createStripDialog(repoagent, rev=rev, opts=opts) @command('^sync|synchronize', [('B', 'bookmarks', False, _('open the bookmark sync window'))], _('thg sync [OPTION]... [PEER]')) def sync(ui, repoagent, url=None, **opts): """synchronize with other repositories""" from tortoisehg.hgqt import bookmark as bookmarkmod, repowidget url = hglib.tounicode(url) if opts.get('bookmarks'): return bookmarkmod.SyncBookmarkDialog(repoagent, url) repo = repoagent.rawRepo() repo.ui.setconfig('tortoisehg', 'defaultwidget', 'sync') w = repowidget.LightRepoWindow(repoagent) if url: w.setSyncUrl(url) 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, _('update 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-4.5.2/tortoisehg/hgqt/rupdate.py0000644000175000017500000001462313153775104021707 0ustar sborhosborho00000000000000# rupdate.py - Remote Update dialog for TortoiseHg # # Copyright 2007 TK Soh # Copyright 2007 Steve Borho # Copyright 2010 Yuki KODAMA # Copyright 2011 Ryan Seto # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. """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 """ from __future__ import absolute_import from .qtcore import ( pyqtSlot, ) from .qtgui import ( QCheckBox, QComboBox, QFormLayout, QSizePolicy, QVBoxLayout, ) from mercurial import error from ..util import hglib from ..util.i18n import _ from . import ( cmdui, csinfo, qtlib, ) class RemoteUpdateWidget(cmdui.AbstractCmdWidget): def __init__(self, repoagent, rev=None, parent=None): super(RemoteUpdateWidget, self).__init__(parent) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self._repoagent = repoagent repo = repoagent.rawRepo() ## main layout form = QFormLayout() form.setContentsMargins(0, 0, 0, 0) form.setSpacing(6) self.setLayout(form) ### target path combo self.path_combo = pcombo = QComboBox() pcombo.setEditable(True) pcombo.addItems([hglib.tounicode(path) for _name, path in repo.ui.configitems('paths')]) form.addRow(_('Location:'), pcombo) ### target revision combo self.rev_combo = combo = QComboBox() combo.setEditable(True) form.addRow(_('Update to:'), combo) combo.addItems(map(hglib.tounicode, hglib.namedbranches(repo))) tags = list(self.repo.tags()) + repo._bookmarks.keys() tags.sort(reverse=True) combo.addItems(map(hglib.tounicode, tags)) 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() form.addRow(_('Target:'), self.target_info) ### Options self.optbox = QVBoxLayout() self.optbox.setSpacing(6) self.optexpander = expander = qtlib.ExpanderLabel(_('Options:'), False) expander.expanded.connect(self.show_options) form.addRow(expander, self.optbox) self.discard_chk = QCheckBox(_('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.addWidget(self.discard_chk) self.optbox.addWidget(self.push_chk) self.optbox.addWidget(self.newbranch_chk) self.optbox.addWidget(self.force_chk) # signal handlers self.rev_combo.editTextChanged.connect(self.update_info) # prepare to show self.push_chk.setHidden(True) self.newbranch_chk.setHidden(True) self.force_chk.setHidden(True) self.update_info() def readSettings(self, qs): self.push_chk.setChecked(qtlib.readBool(qs, 'push')) self.newbranch_chk.setChecked(qtlib.readBool(qs, 'newbranch')) self.optexpander.set_expanded(self.push_chk.isChecked() or self.newbranch_chk.isChecked() or self.force_chk.isChecked()) def writeSettings(self, qs): qs.setValue('push', self.push_chk.isChecked()) qs.setValue('newbranch', self.newbranch_chk.isChecked()) @property def repo(self): return self._repoagent.rawRepo() @pyqtSlot() def update_info(self): new_rev = hglib.fromunicode(self.rev_combo.currentText()) if new_rev == 'null': self.target_info.setText(_('remove working directory')) self.commandChanged.emit() return try: self.target_info.update(self.repo[new_rev]) except (error.LookupError, error.RepoLookupError, error.RepoError): self.target_info.setText(_('unknown revision!')) self.commandChanged.emit() def canRunCommand(self): rev = hglib.fromunicode(self.rev_combo.currentText()) try: return rev in self.repo except error.LookupError: # ambiguous changeid return False def runCommand(self): opts = { 'clean': self.discard_chk.isChecked(), 'push': self.push_chk.isChecked(), 'new_branch': self.newbranch_chk.isChecked(), 'force': self.force_chk.isChecked(), 'd': self.path_combo.currentText(), } # Refer to the revision by the short hash. rev = hglib.fromunicode(self.rev_combo.currentText()) ctx = self.repo[rev] cmdline = hglib.buildcmdargs('rupdate', ctx.hex(), **opts) return self._repoagent.runCommand(cmdline, self) ### Signal Handlers ### def show_options(self, visible): self.push_chk.setVisible(visible) self.newbranch_chk.setVisible(visible) self.force_chk.setVisible(visible) def createRemoteUpdateDialog(repoagent, rev=None, parent=None): dlg = cmdui.CmdControlDialog(parent) dlg.setWindowTitle(_('Remote Update - %s') % repoagent.displayName()) dlg.setWindowIcon(qtlib.geticon('hg-update')) dlg.setObjectName('rupdate') dlg.setRunButtonText(_('&Update')) dlg.setCommandWidget(RemoteUpdateWidget(repoagent, rev, dlg)) return dlg tortoisehg-4.5.2/tortoisehg/hgqt/reporegistry.py0000644000175000017500000010557013153775104023003 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os from .qtcore import ( QFileSystemWatcher, QModelIndex, QPoint, QSettings, QTimer, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAbstractItemView, QAction, QApplication, QDockWidget, QFileDialog, QFontMetrics, QFrame, QMenu, QMessageBox, QTreeView, QVBoxLayout, ) from mercurial import ( commands, hg, util, ) from ..util import ( hglib, paths, ) from ..util.i18n import _ from . import ( qtlib, repotreemodel, settings, ) 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(str) openRequested = pyqtSignal(QModelIndex) removeRequested = pyqtSignal(QModelIndex) dropAccepted = pyqtSignal() def __init__(self, parent): QTreeView.__init__(self, parent, allColumnsShowFocus=True) if qtlib.IS_RETINA: self.setIconSize(qtlib.treeviewRetinaIconSize()) self.msg = '' self.setHeaderHidden(True) self.setExpandsOnDoubleClick(False) self.setMouseTracking(True) # enable drag and drop # see # https://doc.qt.io/qt-4.8/model-view-programming.html#using-drag-and-drop-with-item-views self.setDragEnabled(True) self.setAcceptDrops(True) self.setAutoScroll(True) self.setDragDropMode(QAbstractItemView.DragDrop) self.setDefaultDropAction(Qt.MoveAction) self.setDropIndicatorShown(True) self.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed) self.setSelectionBehavior(QAbstractItemView.SelectRows) 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 keyPressEvent(self, event): if (event.key() in (Qt.Key_Enter, Qt.Key_Return) and self.state() != QAbstractItemView.EditingState): index = self.currentIndex() if index.isValid(): self.openRequested.emit(index) return if event.key() == Qt.Key_Delete: index = self.currentIndex() if index.isValid(): self.removeRequested.emit(index) return super(RepoTreeView, self).keyPressEvent(event) 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: # https://bugreports.qt.io/browse/QTBUG-10180 return super(RepoTreeView, self).mouseMoveEvent(event) def leaveEvent(self, event): if self.msg != '': self.showMessage.emit('') def mouseDoubleClickEvent(self, event): index = self.indexAt(event.pos()) if index.isValid() and index.internalPointer().isRepo(): self.openRequested.emit(index) else: # a double-click on non-repo rows opens an editor super(RepoTreeView, self).mouseDoubleClickEvent(event) def sizeHint(self): size = super(RepoTreeView, self).sizeHint() size.setWidth(QFontMetrics(self.font()).width('M') * 15) return size class RepoRegistryView(QDockWidget): showMessage = pyqtSignal(str) openRepo = pyqtSignal(str, bool) removeRepo = pyqtSignal(str) cloneRepoRequested = pyqtSignal(str) progressReceived = pyqtSignal(str, object, str, str, object) def __init__(self, repomanager, parent): QDockWidget.__init__(self, parent) self._repomanager = repomanager repomanager.repositoryOpened.connect(self._addAndScanRepo) 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.setContextMenuPolicy(Qt.CustomContextMenu) tv.customContextMenuRequested.connect(self._onMenuRequested) tv.showMessage.connect(self.showMessage) tv.openRequested.connect(self._openRepoAt) tv.removeRequested.connect(self._removeAt) 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._loadExpandedState() 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(qtlib.readBool(s, key, defaultmap[key])) 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() s.beginGroup('reporegistry') self._writeExpandedState(s) s.endGroup() def _loadExpandedState(self): s = QSettings() s.beginGroup('reporegistry') self._readExpandedState(s) 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()) 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._loadExpandedState() if activeroot: self.setActiveTabRepo(activeroot) self._reloadModelTimer.stop() def _readExpandedState(self, s): model = self.tview.model() for path in qtlib.readStringList(s, 'expanded'): self.tview.expand(model.indexFromItemPath(path)) def _writeExpandedState(self, s): model = self.tview.model() paths = [model.itemPath(i) for i in model.persistentIndexList() if i.column() == 0 and self.tview.isExpanded(i)] s.setValue('expanded', paths) # TODO: better to handle repositoryOpened signal by model @pyqtSlot(str) def _addAndScanRepo(self, uroot): """Add repo if not exists; called when the workbench has opened it""" uroot = unicode(uroot) 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 addClonedRepo(self, root, sourceroot): """Add repo to the same group as the source""" 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) 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"), 'thg-userconfig', _("View the repository's settings"), self.startSettings), ("remove", _("Re&move from Registry"), 'hg-strip', _("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) @pyqtSlot(QPoint) def _onMenuRequested(self, pos): index = self.tview.currentIndex() if not index.isValid(): return menulist = index.internalPointer().menulist() if not menulist: return self.addtomenu(self.contextmenu, menulist) self.contextmenu.popup(self.tview.viewport().mapToGlobal(pos)) 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 _currentRepoRoot(self): model = self.tview.model() index = self.tview.currentIndex() return model.repoRoot(index) def cloneRepo(self): self.cloneRepoRequested.emit(self._currentRepoRoot()) def explore(self): qtlib.openlocalurl(self._currentRepoRoot()) def terminal(self): model = self.tview.model() index = self.tview.currentIndex() repoitem = index.internalPointer() qtlib.openshell(hglib.fromunicode(model.repoRoot(index)), hglib.fromunicode(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.tview.currentIndex()) self._scanAddedRepo(index) def addSubrepo(self): 'menu action handler for adding a new subrepository' root = self._currentRepoRoot() 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(hglib.loadui(), 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.wvfs('.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.wvfs('.hgsub', 'w') fsub.write(linesep.join(lines) + linesep) fsub.close() if not hasHgsub: commands.add(hglib.loadui(), 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' model = self.tview.model() index = self.tview.currentIndex() path = model.repoRoot(index) root = model.repoRoot(index.parent()) 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 = hglib.fromunicode(self._currentRepoRoot()) sd = settings.SettingsDialog(configrepo=True, focus='web.name', parent=self, root=root) sd.finished.connect(sd.deleteLater) sd.exec_() def openAll(self): index = self.tview.currentIndex() for root in index.internalPointer().childRoots(): self.openRepo.emit(root, False) def open(self, root=None): 'open context menu action, open repowidget unconditionally' if not root: model = self.tview.model() index = self.tview.currentIndex() root = model.repoRoot(index) repotype = index.internalPointer().repotype() else: if os.path.exists(os.path.join(root, '.hg')): repotype = 'hg' else: repotype = 'unknown' if repotype == 'hg': self.openRepo.emit(root, False) else: qtlib.WarningMsgBox( _('Unsupported repository type (%s)') % repotype, _('Cannot open non Mercurial repositories or subrepositories'), parent=self) @pyqtSlot(QModelIndex) def _openRepoAt(self, index): model = self.tview.model() root = model.repoRoot(index) if root: # We can only open mercurial repositories and subrepositories repotype = index.internalPointer().repotype() if repotype == 'hg': self.openRepo.emit(root, True) 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(self._currentRepoRoot()) def startRename(self): self.tview.edit(self.tview.currentIndex()) def newGroup(self): self.tview.model().addGroup(_('New Group')) def removeSelected(self): root = self._currentRepoRoot() self._removeAt(self.tview.currentIndex()) if root: self.removeRepo.emit(root) @pyqtSlot(QModelIndex) def _removeAt(self, index): item = index.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.tview.model() m.removeRows(index.row(), 1, index.parent()) self.updateSettingsFile() def sortbyname(self): index = self.tview.currentIndex() childs = index.internalPointer().childs self.tview.model().sortchilds(childs, lambda x: x.shortname().lower()) def sortbypath(self): index = self.tview.currentIndex() childs = index.internalPointer().childs def keyfunc(x): l = hglib.fromunicode(x.rootpath()) return os.path.normcase(util.normpath(l)) self.tview.model().sortchilds(childs, keyfunc) def sortbyhgsub(self): model = self.tview.model() index = self.tview.currentIndex() ip = index.internalPointer() repo = hg.repository(hglib.loadui(), hglib.fromunicode(model.repoRoot(index))) 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): l = hglib.fromunicode(x.rootpath()) try: return hgsuborder.index(util.normpath(l)) except ValueError: # 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(str) def scanRepo(self, uroot): uroot = unicode(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 paths.is_on_fixed_drive(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-4.5.2/tortoisehg/hgqt/lexers.py0000644000175000017500000001761513150123225021535 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import re from . import ( qsci as Qsci, qtgui as QtGui, ) from . 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-4.5.2/tortoisehg/hgqt/resolve.py0000644000175000017500000004503513163614551021722 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import import os from .qtcore import ( QAbstractTableModel, QItemSelectionModel, QMimeData, QModelIndex, QPoint, QSettings, QUrl, Qt, pyqtSlot, ) from .qtgui import ( QAction, QActionGroup, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QKeySequence, QLabel, QMenu, QMessageBox, QToolButton, QTreeView, QVBoxLayout, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, csinfo, qtlib, thgrepo, visdiff, ) MARGINS = (8, 0, 0, 0) class ResolveDialog(QDialog): def __init__(self, repoagent, parent=None): super(ResolveDialog, self).__init__(parent) self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint | Qt.WindowMaximizeButtonHint) self.setWindowTitle(_('Resolve Conflicts - %s') % repoagent.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 = QTreeView(self) self.utree.setDragDropMode(QTreeView.DragOnly) self.utree.setSelectionMode(QTreeView.ExtendedSelection) self.utree.setSortingEnabled(True) hbox.addWidget(self.utree) self.utree.setContextMenuPolicy(Qt.CustomContextMenu) self.utreecmenu = QMenu(self) mergeactions = QActionGroup(self) mergeactions.triggered.connect(self._mergeByAction) cmauto = self.utreecmenu.addAction(_('Mercurial Re&solve')) cmauto.setToolTip(_('Attempt automatic (trivial) merge')) cmauto.setData('internal:merge') mergeactions.addAction(cmauto) cmmanual = self.utreecmenu.addAction(_('Tool &Resolve')) cmmanual.setToolTip(_('Merge using selected merge tool')) mergeactions.addAction(cmmanual) cmlocal = self.utreecmenu.addAction(_('&Take Local')) cmlocal.setToolTip(_('Accept the local file version (yours)')) cmlocal.setData('internal:local') mergeactions.addAction(cmlocal) cmother = self.utreecmenu.addAction(_('Take &Other')) cmother.setToolTip(_('Accept the other file version (theirs)')) cmother.setData('internal:other') mergeactions.addAction(cmother) cmres = self.utreecmenu.addAction(_('&Mark as Resolved')) cmres.setToolTip(_('Mark this file 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) vbox = QVBoxLayout() vbox.setContentsMargins(*MARGINS) hbox.addLayout(vbox) for action in [cmauto, cmmanual, cmlocal, cmother, cmres]: vbox.addWidget(qtlib.ActionPushButton(action, self)) vbox.addStretch(1) res = qtlib.LabeledSeparator(_('Resolved conflicts')) self.layout().addWidget(res) hbox = QHBoxLayout() hbox.setContentsMargins(*MARGINS) hbox.setSpacing(0) self.layout().addLayout(hbox) self.rtree = QTreeView(self) self.rtree.setDragDropMode(QTreeView.DragOnly) self.rtree.setSelectionMode(QTreeView.ExtendedSelection) self.rtree.setSortingEnabled(True) hbox.addWidget(self.rtree) self.rtree.setContextMenuPolicy(Qt.CustomContextMenu) self.rtreecmenu = QMenu(self) cmedit = self.rtreecmenu.addAction(_('&Edit File')) cmedit.setToolTip(_('Edit resolved file')) cmedit.triggered.connect(self.edit) cmv3way = self.rtreecmenu.addAction(_('3-&Way Diff')) cmv3way.setToolTip(_('Visual three-way diff')) cmv3way.triggered.connect(self.v3way) cmvp0 = self.rtreecmenu.addAction(_('Diff to &Local')) cmvp0.setToolTip(_('Visual diff between resolved file and first ' 'parent')) cmvp0.triggered.connect(self.vp0) cmvp1 = self.rtreecmenu.addAction(_('&Diff to Other')) cmvp1.setToolTip(_('Visual diff between resolved file and second ' 'parent')) cmvp1.triggered.connect(self.vp1) cmures = self.rtreecmenu.addAction(_('Mark as &Unresolved')) cmures.setToolTip(_('Mark this file 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) vbox = QVBoxLayout() vbox.setContentsMargins(*MARGINS) hbox.addLayout(vbox) for action in [cmedit, cmv3way, cmvp0, cmvp1, cmures]: vbox.addWidget(qtlib.ActionPushButton(action, self)) vbox.addStretch(1) 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._cmdlog = cmdui.LogWidget(self) self.layout().addWidget(self._cmdlog) BB = QDialogButtonBox bbox = QDialogButtonBox(BB.Close) bbox.rejected.connect(self.reject) self.layout().addWidget(bbox) self.bbox = bbox s = QSettings() self.restoreGeometry(qtlib.readByteArray(s, 'resolve/geom')) self.refresh() self.utree.selectAll() self.utree.setFocus() repoagent.configChanged.connect(self.tcombo.reset) repoagent.repositoryChanged.connect(self.refresh) @property def repo(self): return self._repoagent.rawRepo() def getSelectedPaths(self, tree): paths = [] 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(map(hglib.tounicode, cmd)) selected = [(r, w) for r, w in selected if r != curroot] if cmdlines: sess = self._repoagent.runCommandSequence(cmdlines, self) self._cmdsession = sess sess.commandFinished.connect(self.refresh) sess.outputReceived.connect(self._cmdlog.appendLog) self._updateActions() def merge(self, tool=False): if not tool: tool = self.tcombo.readValue() cmd = ['resolve'] if tool: cmd += ['--tool='+tool] self.runCommand(self.utree, cmd) @pyqtSlot(QAction) def _mergeByAction(self, action): self.merge(action.data()) 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 refresh(self): 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.Rows for i, path in enumerate(u): if path in paths: smodel.select(model.index(i, 0), sflags) smodel.selectionChanged.connect(self._updateUnresolvedActions) self._updateUnresolvedActions() 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.selectionChanged.connect(self._updateResolvedActions) self._updateResolvedActions() 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() def _updateActions(self): self._updateUnresolvedActions() self._updateResolvedActions() @pyqtSlot() def _updateUnresolvedActions(self): enable = (self.utree.selectionModel().hasSelection() and self._cmdsession.isFinished()) for c in self.umenuitems: c.setEnabled(enable) @pyqtSlot() def _updateResolvedActions(self): enable = (self.rtree.selectionModel().hasSelection() and self._cmdsession.isFinished()) for c in self.rmenuitems: c.setEnabled(enable) merge = len(self.repo[None].parents()) > 1 for c in self.rmmenuitems: c.setEnabled(enable and merge) @pyqtSlot(QPoint) def utreeMenuRequested(self, point): self.utreecmenu.popup(self.utree.viewport().mapToGlobal(point)) @pyqtSlot(QPoint) def rtreeMenuRequested(self, point): self.rtreecmenu.popup(self.rtree.viewport().mapToGlobal(point)) def utreeDoubleClicked(self): if self.repo.ui.configbool('tortoisehg', 'autoresolve', True): self.merge() else: self.merge('internal:merge') 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 None if role == Qt.DisplayRole: data = self.rows[index.row()][index.column()] return hglib.tounicode(data) return None def flags(self, index): flags = super(PathsModel, self).flags(index) if not index.isValid(): return flags flags |= Qt.ItemIsDragEnabled return flags def headerData(self, col, orientation, role=Qt.DisplayRole): if role != Qt.DisplayRole or orientation != Qt.Horizontal: return None else: return 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] def mimeTypes(self): return ['text/uri-list'] def mimeData(self, indexes): paths = [hglib.tounicode(os.path.join(*self.getPathForIndex(i))) for i in indexes if i.column() == 0] data = QMimeData() data.setUrls([QUrl.fromLocalFile(p) for p in paths]) return data 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 @pyqtSlot() 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-4.5.2/tortoisehg/hgqt/archive.py0000644000175000017500000003240513150123225021646 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os from .qtcore import ( pyqtSlot, ) from .qtgui import ( QButtonGroup, QCheckBox, QComboBox, QFileDialog, QFormLayout, QHBoxLayout, QLineEdit, QPushButton, QRadioButton, QSizePolicy, QVBoxLayout, ) from mercurial import error from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, cmdui, qtlib, ) WD_PARENT = _('= Working Directory Parent =') _ARCHIVE_TYPES = [ {'type': 'files', 'ext': '', 'label': _('Directory of files'), 'desc': _('Directory of files')}, {'type': 'tar', 'ext': '.tar', 'label': _('Tar archives'), 'desc': _('Uncompressed tar archive')}, {'type': 'tbz2', 'ext': '.tar.bz2', 'label': _('Bzip2 tar archives'), 'desc': _('Tar archive compressed using bzip2')}, {'type': 'tgz', 'ext': '.tar.gz', 'label': _('Gzip tar archives'), 'desc': _('Tar archive compressed using gzip')}, {'type': 'uzip', 'ext': '.zip', 'label': _('Zip archives'), 'desc': _('Uncompressed zip archive')}, {'type': 'zip', 'ext': '.zip', 'label': _('Zip archives'), 'desc': _('Zip archive compressed using deflate')}, ] class ArchiveWidget(cmdui.AbstractCmdWidget): """Command widget to archive a particular Mercurial revision""" _archive_content_all_files = 0 _archive_content_touched_files = 1 _archive_content_touched_since = 2 def __init__(self, repoagent, rev, parent=None, minrev=None): super(ArchiveWidget, self).__init__(parent) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self._repoagent = repoagent if minrev is None: minrev = rev archive_since = False else: archive_since = True possibleroots = [] if minrev is not None: parents = self.repo[minrev].parents() if parents: for p in parents: text = hglib.tounicode(str(int(p))) possibleroots.append(text) else: possibleroots.append('null') form = QFormLayout() form.setContentsMargins(0, 0, 0, 0) self.setLayout(form) ### content selection ## revision selection self.rev_combo = QComboBox() self.rev_combo.setEditable(True) self.rev_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) form.addRow(_('Revision:'), self.rev_combo) ### content type ## Selection of the content mode self.content_mode = QButtonGroup(self) # All files box = QVBoxLayout() w = QRadioButton(_('All files in this revision'), self) self.content_mode.addButton(w, self._archive_content_all_files) box.addWidget(w) # Touched in this revision w = QRadioButton(_('Only files modified/created in this revision'), self) self.content_mode.addButton(w, self._archive_content_touched_files) box.addWidget(w) # Touched since sincebox = QHBoxLayout() self.rootrev_combo = QComboBox(self) self.rootrev_combo.setEditable(True) self.rootrev_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) if minrev is not None: for text in possibleroots: self.rootrev_combo.addItem(text) self.rootrev_combo.setCurrentIndex(0) w = QRadioButton(_('Only files modified/created since:'), self) self.content_mode.addButton(w, self._archive_content_touched_since) sincebox.addWidget(w) sincebox.addWidget(self.rootrev_combo) box.addLayout(sincebox) if archive_since: # default to "touched since X" if the input is a range self.content_mode.button(self._archive_content_touched_since).setChecked(True) else: self.content_mode.button(self._archive_content_all_files).setChecked(True) form.addRow(_('Archive Content:'), box) # subrepository option self.subrepos_chk = QCheckBox(_('Recurse into subrepositories')) form.addRow('', self.subrepos_chk) # selecting a destination self.dest_edit = QLineEdit() self.dest_edit.setMinimumWidth(300) self.dest_btn = QPushButton(_('Browse...')) self.dest_btn.setAutoDefault(False) box = QHBoxLayout() box.addWidget(self.dest_edit) box.addWidget(self.dest_btn) form.addRow(_('Destination path:'), box) # archive type selection self._typesradios = QButtonGroup(self) box = QVBoxLayout() for i, spec in enumerate(_ARCHIVE_TYPES): w = QRadioButton(spec['desc'], self) self._typesradios.addButton(w, i) box.addWidget(w) form.addRow(_('Archive types:'), box) # some extras self.hgcmd_txt = QLineEdit() self.hgcmd_txt.setReadOnly(True) form.addRow(_('Hg command:'), self.hgcmd_txt) # set default values self.prevtarget = None self.rev_combo.addItem(WD_PARENT) self.rev_combo.addItems(map(hglib.tounicode, hglib.namedbranches(self.repo))) 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._typesradios.button(0).setChecked(True) self.update_path() # connecting slots self.dest_edit.textEdited.connect(self.compose_command) self.rev_combo.editTextChanged.connect(self.rev_combo_changed) self.dest_btn.clicked.connect(self.browse_clicked) self.content_mode.buttonClicked.connect(self.compose_command) self.rootrev_combo.editTextChanged.connect(self.compose_command) self.subrepos_chk.toggled.connect(self.compose_command) self._typesradios.buttonClicked.connect(self.update_path) @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 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, _filter = FD.getSaveFileName( self, caption, dest, filter, None, FD.ReadOnly) if response: self.dest_edit.setText(response) self.update_path() 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_rootrev(self): rev = self.rootrev_combo.currentText() return hglib.fromunicode(rev) def get_selected_archive_type(self): """Return a dictionary describing the selected archive type""" return _ARCHIVE_TYPES[self._typesradios.checkedId()] 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: self.commandChanged.emit() return wdrev = str(self.repo['.'].rev()) if text == WD_PARENT: text = wdrev else: try: self.repo[hglib.fromunicode(text)] except (error.RepoError, error.LookupError): self.commandChanged.emit() 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 self.compose_command() @pyqtSlot() def compose_command(self): content = self.content_mode.checkedId() targetrev = hglib.tounicode(self.get_selected_rev()) if content == self._archive_content_all_files: incl = None elif content == self._archive_content_touched_files: incl = 'set:added() or modified()' elif content == self._archive_content_touched_since: expr = 'set:status(%s, %s, added() or modified())' rootrev = hglib.tounicode(self.get_selected_rootrev()) incl = hglib.formatfilespec(expr, rootrev, targetrev) else: assert False cmdline = hglib.buildcmdargs('archive', self.dest_edit.text(), r=targetrev, S=self.subrepos_chk.isChecked(), I=incl, t=self.get_selected_archive_type()['type']) self.hgcmd_txt.setText('hg ' + hglib.prettifycmdline(cmdline)) self.commandChanged.emit() return cmdline def canRunCommand(self): rev = self.get_selected_rev() if not rev or not self.dest_edit.text(): return False try: return rev in self.repo except error.LookupError: # ambiguous changeid return False def runCommand(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 cmdcore.nullCmdSession() 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 cmdcore.nullCmdSession() 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 cmdcore.nullCmdSession() else: qtlib.WarningMsgBox(_('Duplicate Name'), _('The destination "%s" already exists as ' 'a folder!') % dest) return cmdcore.nullCmdSession() cmdline = self.compose_command() return self._repoagent.runCommand(cmdline, self) def createArchiveDialog(repoagent, rev=None, parent=None, minrev=None): dlg = cmdui.CmdControlDialog(parent) dlg.setWindowTitle(_('Archive - %s') % repoagent.displayName()) dlg.setWindowIcon(qtlib.geticon('hg-archive')) dlg.setObjectName('archive') dlg.setRunButtonText(_('&Archive')) dlg.setCommandWidget(ArchiveWidget(repoagent, rev, dlg, minrev)) return dlg tortoisehg-4.5.2/tortoisehg/hgqt/customtools.py0000644000175000017500000011620013153775104022630 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import re from .qtcore import ( QSettings, Qt, ) from .qtgui import ( QComboBox, QDialog, QFormLayout, QFrame, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QPushButton, QTableWidget, QTableWidgetItem, QVBoxLayout, ) from ..util import hglib from ..util.i18n import _ from . import qtlib DEFAULTICONNAME = 'tools-spanner-hammer' 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) 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) 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 = 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] # value may be bool if originating from hglib.tortoisehgtools() if value != '': ini.set(section, keyname, str(value)) # 2. Save the new guidefs for n, toollistwidget in enumerate(self.widgets): toollocation = self.locationcombo.itemData(n) 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]: if keyname: keyname = '%s.%s' % (hooktype, keyname) else: keyname = hooktype try: 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) 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 = 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, tooltips=None): index = 0 if selecteditem: try: index = list(items).index(selecteditem) except ValueError: pass combo = QComboBox() combo.addItems(items) if index: combo.setCurrentIndex(index) if tooltips: for idx, tooltip in enumerate(tooltips): combo.setItemData(idx, tooltip, Qt.ToolTipRole) 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( qtlib.readByteArray(s, self.dialogname + '/geom')) 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'), ] _defaulticonstring = _('') def __init__(self, parent=None, toolname=None, toolconfig={}): super(CustomToolConfigDialog, self).__init__(parent, dialogname='customtools', windowTitle=_('Configure Custom Tool'), windowIcon=qtlib.geticon(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' 'Common variables:\n' '- {ROOT}: The path to the current repository root.\n' '- {REV} / {REVID}: Selected revisions numbers / hexadecimal' ' revision id hashes respectively formatted as a revset' ' expression.\n' '- {SELECTEDFILES}: The list of files selected by the user on the ' 'revision details file list.\n' '- {FILES}: The list of files touched by the selected revisions.\n' '- {ALLFILES}: All the files tracked by Mercurial on the selected' ' revisions.\n' 'Pair selection variables:\n' '- {REV_A} / {REVID_A}: the first selected revision number / ' 'hexadecimal revision id hash respectively.\n' '- {REV_B} / {REVID_B}: the second selected revision number / ' 'hexadecimal revision id hash respectively.\n')) 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(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', ) _hooktooltips = ( _('Run after a changegroup has been added via push, pull or unbundle. ' 'ID of the first new changeset is in $HG_NODE and last in ' '$HG_NODE_LAST. URL from which changes came is in ' '$HG_URL.'), _('Run after a changeset has been created in the local repository. ID ' 'of the newly created changeset is in $HG_NODE. Parent ' 'changeset IDs are in $HG_PARENT1 and ' '$HG_PARENT2.'), _('Run after a changeset has been pulled, pushed, or unbundled into ' 'the local repository. The ID of the newly arrived changeset is in ' '$HG_NODE. URL that was source of changes came is in ' '$HG_URL.'), _('Run after sending changes from local repository to another. ID of ' 'first changeset sent is in $HG_NODE. Source of operation ' 'is in $HG_SOURCE.'), _('Run before a changegroup is added via push, pull or unbundle. Exit ' 'status 0 allows the changegroup to proceed. Non-zero status will ' 'cause the push, pull or unbundle to fail. URL from which changes ' 'will come is in $HG_URL.'), _('Run before starting a local commit. Exit status 0 allows the commit ' 'to proceed. Non-zero status will cause the commit to fail. Parent ' 'changeset IDs are in $HG_PARENT1 and ' '$HG_PARENT2.'), _('Run before listing pushkeys (like bookmarks) in the repository. ' 'Non-zero status will cause failure. The key namespace is in ' '$HG_NAMESPACE.'), _('Run before collecting changes to send from the local repository to ' 'another. Non-zero status will cause failure. This lets you ' 'prevent pull over HTTP or SSH. Also prevents against local pull, ' 'push (outbound) or bundle commands, but not effective, since you ' 'can just copy files instead then. Source of operation is in ' '$HG_SOURCE. If "serve", operation is happening on behalf ' 'of remote SSH or HTTP repository. If "push", "pull" or "bundle", ' 'operation is happening on behalf of repository on same system.'), _('Run before a pushkey (like a bookmark) is added to the repository. ' 'Non-zero status will cause the key to be rejected. The key ' 'namespace is in $HG_NAMESPACE, the key is in ' '$HG_KEY, the old value (if any) is in $HG_OLD, ' 'and the new value is in $HG_NEW.'), _('Run before creating a tag. Exit status 0 allows the tag to be ' 'created. Non-zero status will cause the tag to fail. ID of ' 'changeset to tag is in $HG_NODE. Name of tag is in ' '$HG_TAG. Tag is local if $HG_LOCAL=1, in ' 'repository if $HG_LOCAL=0.'), _('Run after a changegroup has been added via push, pull or unbundle, ' 'but before the transaction has been committed. Changegroup is ' 'visible to hook program. This lets you validate incoming changes ' 'before accepting them. Passed the ID of the first new changeset ' 'in $HG_NODE and last in $HG_NODE_LAST. Exit ' 'status 0 allows the transaction to commit. Non-zero status will ' 'cause the transaction to be rolled back and the push, pull or ' 'unbundle will fail. URL that was source of changes is in ' '$HG_URL.'), _('Run after a changeset has been created but the transaction not yet ' 'committed. Changeset is visible to hook program. This lets you ' 'validate commit message and changes. Exit status 0 allows the ' 'commit to proceed. Non-zero status will cause the transaction to ' 'be rolled back. ID of changeset is in $HG_NODE. Parent ' 'changeset IDs are in $HG_PARENT1 and ' '$HG_PARENT2.'), _('Run before updating the working directory. Exit status 0 allows the ' 'update to proceed. Non-zero status will prevent the update. ' 'Changeset ID of first new parent is in $HG_PARENT1. ' 'If merge, ID of second new parent is in $HG_PARENT2.'), _('Run after listing pushkeys (like bookmarks) in the repository. The ' 'key namespace is in $HG_NAMESPACE. $HG_VALUES ' 'is a dictionary containing the keys and values.'), _('Run after a pushkey (like a bookmark) is added to the repository. ' 'The key namespace is in $HG_NAMESPACE, the key is in ' '$HG_KEY, the old value (if any) is in $HG_OLD, ' 'and the new value is in $HG_NEW.'), _('Run after a tag is created. ID of tagged changeset is in ' '$HG_NODE. Name of tag is in $HG_TAG. Tag is ' 'local if $HG_LOCAL=1, in repository if ' '$HG_LOCAL=0.'), _('Run after updating the working directory. Changeset ID of first new ' 'parent is in $HG_PARENT1. If merge, ID of second new ' 'parent is in $HG_PARENT2. If the update succeeded, ' '$HG_ERROR=0. If the update failed (e.g. because ' 'conflicts not resolved), $HG_ERROR=1.'), ) _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._hooktooltips) 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 tortoisehg-4.5.2/tortoisehg/hgqt/hgemail.ui0000644000175000017500000004610013150123225021615 0ustar sborhosborho00000000000000 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-4.5.2/tortoisehg/hgqt/cslist.py0000644000175000017500000001521713150123225021530 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os from .qtcore import ( Qt, pyqtSlot, ) from .qtgui import ( QCheckBox, QHBoxLayout, QLabel, QLayout, QScrollArea, QSizePolicy, QVBoxLayout, QWidget, ) from ..util.i18n import _ from ..util.patchctx import patchctx from . import ( csinfo, qtlib, ) _SPACING = 6 class ChangesetList(QWidget): def __init__(self, repo=None, parent=None): super(ChangesetList, self).__init__(parent) 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(self._updateView) # 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.setParent(None) 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 not self.curitems: 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 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 @pyqtSlot() def _updateView(self): self.update(self.curitems) tortoisehg-4.5.2/tortoisehg/hgqt/thgrepo.py0000644000175000017500000012160313150123225021674 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import sys import shutil import tempfile import re import time from .qtcore import ( QFile, QFileSystemWatcher, QIODevice, QObject, QSignalMapper, pyqtSignal, pyqtSlot, ) from hgext import mq from mercurial import ( bundlerepo, error, extensions, filemerge, hg, localrepo, node, subrepo, ) from ..util import ( hglib, paths, ) from ..util.patchctx import patchctx from . import cmdcore _repocache = {} _kbfregex = re.compile(r'^\.kbf/') _lfregex = re.compile(r'^\.hglf/') # thgrepo.repository() will be deprecated def repository(_ui=None, path=''): '''Returns a subclassed Mercurial repository to which new THG-specific methods have been added. The repository object is obtained using mercurial.hg.repository()''' if path not in _repocache: if _ui is None: _ui = hglib.loadui() try: repo = hg.repository(_ui, path) repo = repo.unfiltered() repo.__class__ = _extendrepo(repo) repo = repo.filtered('visible') 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] def _filteredrepo(repo, hiddenincluded): if hiddenincluded: return repo.unfiltered() else: return repo.filtered('visible') # flags describing changes that could occur in repository LogChanged = 0x1 WorkingParentChanged = 0x2 WorkingBranchChanged = 0x4 WorkingStateChanged = 0x8 # internal flag to invalidate dirstate cache _PollDeferred = 0x1 # flag to defer polling _PollFsChangesPending = 0x2 _PollStatusPending = 0x4 class RepoWatcher(QObject): """Notify changes of repository by optionally monitoring filesystem""" configChanged = pyqtSignal() repositoryChanged = pyqtSignal(int) repositoryDestroyed = pyqtSignal() def __init__(self, repo, parent=None): super(RepoWatcher, self).__init__(parent) self._repo = repo self._ui = repo.ui self._fswatcher = None self._deferredpoll = 0 # _Poll* flags self._filesmap = {} # path: (flag, watched) self._datamap = {} # readmeth: (flag, dep-path) self._laststats = {} # path: (size, ctime, mtime) self._lastdata = {} # readmeth: content self._fixState() 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._onFsChanged) self._fswatcher.fileChanged.connect(self._onFsChanged) 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 This will release OS resources held by filesystem watcher, so good for disabling change notification for a long time. """ 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(): self._ui.debug('failed to remove paths - destroying watcher\n') 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() def resumeStatusPolling(self): """Execute deferred status checks to emit notification signals""" self._deferredpoll &= ~_PollDeferred if self._deferredpoll & _PollFsChangesPending: self._pollFsChanges() self._deferredpoll &= ~(_PollFsChangesPending | _PollStatusPending) if self._deferredpoll & _PollStatusPending: self._pollStatus() self._deferredpoll &= ~_PollStatusPending def suspendStatusPolling(self): """Defer status checks until resumed Resuming from suspended state should be cheaper, but no OS resources will be released. This is good for short-time suspend. """ self._deferredpoll |= _PollDeferred @pyqtSlot() def _onFsChanged(self): if self._deferredpoll: self._ui.debug('filesystem change detected, but poll deferred\n') self._deferredpoll |= _PollFsChangesPending return self._pollFsChanges() def _pollFsChanges(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, (_flag, watched) in self._filesmap.iteritems() if watched and f in self._laststats] files = [unicode(f) for f in self._fswatcher.files()] for f in existing: if hglib.tounicode(f) not in files: self._ui.debug('add file to watcher: %s\n' % 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: self._ui.debug('add ui file to watcher: %s\n' % f) self._fswatcher.addPath(hglib.tounicode(f)) def clearStatus(self): self._laststats.clear() self._lastdata.clear() def pollStatus(self): if self._deferredpoll: self._ui.debug('poll request deferred\n') self._deferredpoll |= _PollStatusPending return self._pollStatus() def _pollStatus(self): if not os.path.exists(self._repo.path): self._ui.debug('repository destroyed: %s\n' % self._repo.root) self.repositoryDestroyed.emit() return if self._locked(): self._ui.debug('locked, aborting\n') return curstats, curdata = self._readState() changeflags = self._calculateChangeFlags(curstats, curdata) if self._locked(): self._ui.debug('lock still held - ignoring for now\n') return self._laststats = curstats self._lastdata = curdata if changeflags: self._ui.debug('change found (flags = 0x%x)\n' % changeflags) self.repositoryChanged.emit(changeflags) # may update repo paths self._fixState() self._checkuimtime() def _locked(self): if os.path.lexists(self._repo.vfs.join('wlock')): return True if os.path.lexists(self._repo.svfs.join('lock')): return True return False def _fixState(self): """Update paths to be checked and record state of new paths""" repo = self._repo q = getattr(repo, 'mq', None) newfilesmap = { repo.vfs.join('bookmarks'): (LogChanged, False), repo.vfs.join('bookmarks.current'): (LogChanged, False), repo.vfs.join('branch'): (0, False), repo.vfs.join('dirstate'): (WorkingStateChanged, False), repo.vfs.join('localtags'): (LogChanged, False), repo.svfs.join('00changelog.i'): (LogChanged, False), repo.svfs.join('obsstore'): (LogChanged, False), repo.svfs.join('phaseroots'): (LogChanged, False), } if q: newfilesmap.update({ q.join('guards'): (LogChanged, True), q.join('series'): (LogChanged, True), q.join('status'): (LogChanged, True), repo.vfs.join('patches.queue'): (LogChanged, True), repo.vfs.join('patches.queues'): (LogChanged, True), }) newpaths = set(newfilesmap) - set(self._filesmap) if not newpaths: return self._filesmap = newfilesmap self._datamap = { RepoWatcher._readbranch: (WorkingBranchChanged, repo.vfs.join('branch')), RepoWatcher._readparents: (WorkingParentChanged, repo.vfs.join('dirstate')), } newstats, newdata = self._readState(newpaths) self._laststats.update(newstats) self._lastdata.update(newdata) def _readState(self, targetpaths=None): if targetpaths is None: targetpaths = self._filesmap curstats = {} for path in targetpaths: try: # see mercurial.util.filestat for details what attributes # are needed an how ambiguity is resolved st = os.stat(path) curstats[path] = (st.st_size, st.st_ctime, st.st_mtime) except EnvironmentError: pass curdata = {} for readmeth, (_flag, path) in self._datamap.iteritems(): if path not in targetpaths: continue last = self._laststats.get(path, -1) cur = curstats.get(path, -1) if last != cur: try: curdata[readmeth] = readmeth(self) except EnvironmentError: pass elif cur >= 0 and readmeth in self._lastdata: curdata[readmeth] = self._lastdata[readmeth] return curstats, curdata def _calculateChangeFlags(self, curstats, curdata): changeflags = 0 for path, (flag, _watched) in self._filesmap.iteritems(): last = self._laststats.get(path, -1) cur = curstats.get(path, -1) if last != cur: self._ui.debug(' stat: %s (%r -> %r)\n' % (path, last, cur)) changeflags |= flag for readmeth, (flag, _path) in self._datamap.iteritems(): last = self._lastdata.get(readmeth) cur = curdata.get(readmeth) if last != cur: self._ui.debug(' data: %s (%r -> %r)\n' % (readmeth.__name__, last, cur)) changeflags |= flag return changeflags def _readparents(self): return self._repo.vfs('dirstate').read(40) def _readbranch(self): return self._repo.vfs('branch').read() 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: self._ui.debug('config change detected\n') self._uimtime = mtime self.configChanged.emit() except (EnvironmentError, ValueError): pass class RepoAgent(QObject): """Proxy access to repository and keep its states up-to-date""" # change notifications are not emitted while command is running because # repository files are likely to be modified configChanged = pyqtSignal() repositoryChanged = pyqtSignal(int) repositoryDestroyed = pyqtSignal() serviceStopped = pyqtSignal() busyChanged = pyqtSignal(bool) commandFinished = pyqtSignal(cmdcore.CmdSession) outputReceived = pyqtSignal(str, str) progressReceived = pyqtSignal(cmdcore.ProgressMessage) def __init__(self, repo): QObject.__init__(self) self._repo = self._baserepo = repo # TODO: remove repo-to-agent references later; all widgets should own # RepoAgent instead of thgrepository. repo._pyqtobj = self # base repository for bundle or union (set in dispatch._dispatch) repo.ui.setconfig('bundle', 'mainreporoot', repo.root) # keep url separately from repo.url() because it is abbreviated to # relative path to cwd in bundle or union repo self._overlayurl = '' self._repochanging = 0 self._watcher = watcher = RepoWatcher(repo, self) watcher.configChanged.connect(self._onConfigChanged) watcher.repositoryChanged.connect(self._onRepositoryChanged) watcher.repositoryDestroyed.connect(self._onRepositoryDestroyed) self._cmdagent = cmdagent = cmdcore.CmdAgent(repo.ui, self, cwd=self.rootPath()) cmdagent.outputReceived.connect(self.outputReceived) cmdagent.progressReceived.connect(self.progressReceived) cmdagent.serviceStopped.connect(self._tryEmitServiceStopped) cmdagent.busyChanged.connect(self._onBusyChanged) cmdagent.commandFinished.connect(self._onCommandFinished) self._subrepoagents = {} # path: agent def startMonitoringIfEnabled(self): """Start filesystem monitoring on repository open by RepoManager""" repo = self._repo ui = repo.ui monitorrepo = repo.ui.config('tortoisehg', 'monitorrepo', 'localonly') if monitorrepo == 'never': ui.debug('watching of F/S events is disabled by configuration\n') elif (monitorrepo == 'localonly' and not paths.is_on_fixed_drive(repo.path)): ui.debug('not watching F/S events for network drive\n') else: self._watcher.startMonitoring() def isServiceRunning(self): return self._watcher.isMonitoring() or self._cmdagent.isServiceRunning() def stopService(self): """Shut down back-end services on repository closed by RepoManager""" if self._watcher.isMonitoring(): self._watcher.stopMonitoring() self._tryEmitServiceStopped() self._cmdagent.stopService() @pyqtSlot() def _tryEmitServiceStopped(self): if not self.isServiceRunning(): self.serviceStopped.emit() def suspendMonitoring(self): """Stop filesystem monitoring and release OS resources""" self._watcher.stopMonitoring() def resumeMonitoring(self): """Resume filesystem monitoring if possible""" if self._watcher.isMonitoring(): return self.pollStatus() self.startMonitoringIfEnabled() def rawRepo(self): return self._repo def rootPath(self): return hglib.tounicode(self._repo.root) def displayName(self): """Name for window titles and similar""" if self._repo.ui.configbool('tortoisehg', 'fullpath'): return self.rootPath() else: return self.shortName() def shortName(self): """Name for tables, tabs, and sentences""" webname = hglib.shortreponame(self._repo.ui) if webname: return hglib.tounicode(webname) else: return os.path.basename(self.rootPath()) def hiddenRevsIncluded(self): return self._repo.filtername != 'visible' def setHiddenRevsIncluded(self, included): """Switch visibility of hidden (i.e. pruned) changesets""" if self.hiddenRevsIncluded() == included: return self._changeRepo(_filteredrepo(self._repo, included)) self._flushRepositoryChanged() def overlayUrl(self): return self._overlayurl def setOverlay(self, url): """Switch to bundle or union repository overlaying this""" url = unicode(url) if self._overlayurl == url: return repo = hg.repository(self._baserepo.ui, hglib.fromunicode(url)) if repo.root != self._baserepo.root: raise ValueError('invalid overlay repository: %s' % url) repo = repo.unfiltered() repo.__class__ = _extendrepo(repo) repo._pyqtobj = self # TODO: remove repo-to-agent references repo = repo.filtered('visible') self._changeRepo(_filteredrepo(repo, self.hiddenRevsIncluded())) self._overlayurl = url self._watcher.suspendStatusPolling() self._flushRepositoryChanged() def clearOverlay(self): if not self._overlayurl: return repo = self._baserepo repo.thginvalidate() # take changes during overlaid self._changeRepo(_filteredrepo(repo, self.hiddenRevsIncluded())) self._overlayurl = '' self._watcher.resumeStatusPolling() self._flushRepositoryChanged() def _changeRepo(self, repo): # bundle/union repo will append temporary revisions to changelog self._repochanging = LogChanged self._repo = repo def _emitRepositoryChanged(self, flags): flags |= self._repochanging self._repochanging = 0 self.repositoryChanged.emit(flags) def _flushRepositoryChanged(self): if self._cmdagent.isBusy(): return # delayed until _onBusyChanged(False) if self._repochanging: self._emitRepositoryChanged(0) def clearStatus(self): """Forget last status so that next poll should emit change signals""" self._watcher.clearStatus() def pollStatus(self): """Force checking changes to emit corresponding signals; this will be deferred if command is running""" self._watcher.pollStatus() self._flushRepositoryChanged() @pyqtSlot() def _onConfigChanged(self): self._repo.invalidateui() assert not self._cmdagent.isBusy() self._cmdagent.stopService() # to reload config self.configChanged.emit() @pyqtSlot(int) def _onRepositoryChanged(self, flags): self._repo.thginvalidate() # ignore signal that just contains internal flags if flags & ~WorkingStateChanged: self._emitRepositoryChanged(flags) @pyqtSlot() def _onRepositoryDestroyed(self): if self._repo.root in _repocache: del _repocache[self._repo.root] # avoid further changed/destroyed signals self._watcher.stopMonitoring() self.repositoryDestroyed.emit() def isBusy(self): return self._cmdagent.isBusy() def _preinvalidateCache(self): if self._cmdagent.isBusy(): # 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._watcher.suspendStatusPolling() else: self._watcher.resumeStatusPolling() if not self._watcher.isMonitoring(): # detect changes made by the last command even if monitoring # is disabled self._watcher.pollStatus() self._flushRepositoryChanged() self.busyChanged.emit(busy) def runCommand(self, cmdline, uihandler=None, overlay=True): """Executes a single command asynchronously in this repository""" cmdline = self._extendCmdline(cmdline, overlay) return self._cmdagent.runCommand(cmdline, uihandler) def runCommandSequence(self, cmdlines, uihandler=None, overlay=True): """Executes a series of commands asynchronously in this repository""" cmdlines = [self._extendCmdline(l, overlay) for l in cmdlines] return self._cmdagent.runCommandSequence(cmdlines, uihandler) def _extendCmdline(self, cmdline, overlay): if self.hiddenRevsIncluded(): cmdline = ['--hidden'] + cmdline if overlay and self._overlayurl: cmdline = ['-R', self._overlayurl] + cmdline return cmdline def abortCommands(self): """Abort running and queued commands""" self._cmdagent.abortCommands() @pyqtSlot(cmdcore.CmdSession) def _onCommandFinished(self, sess): self._preinvalidateCache() self.commandFinished.emit(sess) def subRepoAgent(self, path): """Return RepoAgent of sub or patch repository""" root = self.rootPath() path = hglib.normreporoot(os.path.join(root, path)) if path == root or not path.startswith(root.rstrip(os.sep) + os.sep): # only sub path is allowed to avoid circular references raise ValueError('invalid sub path: %s' % path) try: return self._subrepoagents[path] except KeyError: pass manager = self.parent() if not manager: raise RuntimeError('cannot open sub agent of unmanaged repo') assert isinstance(manager, RepoManager) self._subrepoagents[path] = agent = manager.openRepoAgent(path) return agent def releaseSubRepoAgents(self): """Release RepoAgents referenced by this when repository closed by RepoManager""" if not self._subrepoagents: return manager = self.parent() if not manager: raise RuntimeError('cannot release sub agents of unmanaged repo') assert isinstance(manager, RepoManager) for path in self._subrepoagents: manager.releaseRepoAgent(path) self._subrepoagents.clear() class RepoManager(QObject): """Cache open RepoAgent instances and bundle their signals""" repositoryOpened = pyqtSignal(str) repositoryClosed = pyqtSignal(str) configChanged = pyqtSignal(str) repositoryChanged = pyqtSignal(str, int) repositoryDestroyed = pyqtSignal(str) busyChanged = pyqtSignal(str, bool) progressReceived = pyqtSignal(str, cmdcore.ProgressMessage) _SIGNALMAP = [ # source, dest ('configChanged', 'configChanged'), ('repositoryDestroyed', 'repositoryDestroyed'), ('serviceStopped', '_tryCloseRepoAgent'), ('busyChanged', '_mapBusyChanged'), ] def __init__(self, ui, parent=None): super(RepoManager, self).__init__(parent) self._ui = ui self._openagents = {} # path: (agent, refcount) # refcount=0 means the repo is about to be closed self._sigmappers = [] for _sig, slot in self._SIGNALMAP: mapper = QSignalMapper(self) self._sigmappers.append(mapper) mapper.mapped[str].connect(getattr(self, slot)) def openRepoAgent(self, path): """Return RepoAgent for the specified path and increment refcount""" path = hglib.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): getattr(agent, sig).connect(mapper.map) mapper.setMapping(agent, agent.rootPath()) agent.repositoryChanged.connect(self._mapRepositoryChanged) agent.progressReceived.connect(self._mapProgressReceived) agent.startMonitoringIfEnabled() assert agent.rootPath() == path self._openagents[path] = (agent, 1) self.repositoryOpened.emit(path) return agent @pyqtSlot(str) def releaseRepoAgent(self, path): """Decrement refcount of RepoAgent and close it if possible""" path = hglib.normreporoot(path) agent, refcount = self._openagents[path] self._openagents[path] = (agent, refcount - 1) if refcount > 1: return # close child agents first, which may reenter to releaseRepoAgent() agent.releaseSubRepoAgents() if agent.isServiceRunning(): self._ui.debug('stopping service: %s\n' % hglib.fromunicode(path)) agent.stopService() else: self._tryCloseRepoAgent(path) @pyqtSlot(str) def _tryCloseRepoAgent(self, path): path = unicode(path) agent, refcount = self._openagents[path] if refcount > 0: # repo may be reopen before its services stopped return self._ui.debug('closing repo: %s\n' % hglib.fromunicode(path)) del self._openagents[path] # TODO: disconnected automatically if _repocache does not exist for (sig, _slot), mapper in zip(self._SIGNALMAP, self._sigmappers): getattr(agent, sig).disconnect(mapper.map) mapper.removeMappings(agent) agent.repositoryChanged.disconnect(self._mapRepositoryChanged) agent.progressReceived.disconnect(self._mapProgressReceived) 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 = hglib.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() @pyqtSlot(int) def _mapRepositoryChanged(self, flags): agent = self.sender() assert isinstance(agent, RepoAgent) self.repositoryChanged.emit(agent.rootPath(), flags) @pyqtSlot(str) def _mapBusyChanged(self, path): agent, _refcount = self._openagents[unicode(path)] self.busyChanged.emit(path, agent.isBusy()) @pyqtSlot(cmdcore.ProgressMessage) def _mapProgressReceived(self, progress): agent = self.sender() assert isinstance(agent, RepoAgent) self.progressReceived.emit(agent.rootPath(), progress) _uiprops = '''_uifiles postpull tabwidth maxdiff deadbranches _exts _thghiddentags summarylen mergetools'''.split() _thgrepoprops = '''_thgmqpatchnames thgmqunappliedpatches'''.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) # If changeid is a basectx, repo[changeid] returns the same object. # We assumes changectx is already wrapped in that case; otherwise, # changectx would be double wrapped by thgchangectx. changectx = super(thgrepository, self).__getitem__(changeid) if changectx is changeid: return changectx 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) @localrepo.unfilteredpropertycache def _thghiddentags(self): ht = self.ui.config('tortoisehg', 'hidetags', '') return [t.strip() for t in ht.split()] @localrepo.unfilteredpropertycache 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] @localrepo.unfilteredpropertycache 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) @localrepo.unfilteredpropertycache 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.vfs.join('hgrc')) return files @localrepo.unfilteredpropertycache 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 @localrepo.unfilteredpropertycache def postpull(self): pp = self.ui.config('tortoisehg', 'postpull') if pp in ('rebase', 'update', 'fetch', 'updateorrebase'): return pp return 'none' @localrepo.unfilteredpropertycache 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 @localrepo.unfilteredpropertycache 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 @localrepo.unfilteredpropertycache 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 @localrepo.unfilteredpropertycache def deadbranches(self): db = self.ui.config('tortoisehg', 'deadbranch', '') return [b.strip() for b in db.split(',')] @localrepo.unfilteredpropertycache 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 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.vfs.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.invalidatedirstate() 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 localrepo.hasunfilteredcache(self, 'mq'): delattr(self.unfiltered(), 'mq') for a in _thgrepoprops + _uiprops: if localrepo.hasunfilteredcache(self, a): delattr(self.unfiltered(), a) def invalidateui(self): 'Should be called when mtime of ui files are changed' origui = self.ui self.ui = hglib.loadui() self.ui.readconfig(self.vfs.join('hgrc')) hglib.copydynamicconfig(origui, self.ui) for a in _uiprops: if localrepo.hasunfilteredcache(self, a): delattr(self.unfiltered(), a) 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.vfs.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 bfStandin(self, path): return '.kbf/' + path def lfStandin(self, path): return '.hglf/' + path return thgrepository _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 assert parentcls not in _changectxclscache.values(), 'double thgchangectx' _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 r = r.unfiltered() r.__class__ = _extendrepo(r) srepo._repo = r.filtered('visible') 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 _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 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 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 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 = hglib.readmergestate(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 (hglib.fromunicode(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-4.5.2/tortoisehg/hgqt/merge.py0000644000175000017500000006030613153775104021341 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import from .qtcore import ( QSettings, QSize, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAction, QCheckBox, QDialog, QHBoxLayout, QLabel, QMessageBox, QProgressBar, QPushButton, QVBoxLayout, QWizard, QWizardPage, ) from ..util import hglib from ..util.i18n import _ from . import ( csinfo, cmdcore, cmdui, commit, messageentry, qscilib, qtlib, resolve, status, thgrepo, wctxcleaner, ) MARGINS = (8, 0, 0, 0) class MergeDialog(QWizard): def __init__(self, repoagent, otherrev, parent=None): super(MergeDialog, self).__init__(parent) self._repoagent = repoagent f = self.windowFlags() self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint) self.setWindowTitle(_('Merge - %s') % repoagent.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, str(otherrev), self) self.addPage(summarypage) self.addPage(MergePage(repoagent, str(otherrev), 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) self._readSettings() def _readSettings(self): qs = QSettings() qs.beginGroup('merge') for n in ['autoadvance', 'skiplast']: self.setField(n, qs.value(n, False)) repo = self._repoagent.rawRepo() n = 'autoresolve' self.setField(n, repo.ui.configbool('tortoisehg', n, qtlib.readBool(qs, n, True))) qs.endGroup() def _writeSettings(self): qs = QSettings() qs.beginGroup('merge') for n in ['autoadvance', 'autoresolve', 'skiplast']: qs.setValue(n, self.field(n)) qs.endGroup() @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() def done(self, r): self._writeSettings() super(MergeDialog, self).done(r) 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[None].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, otherrev, 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) 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) otherCsInfo = create(otherrev) self.layout().addWidget(otherCsInfo) self.otherCsInfo = otherCsInfo ## current revision local_sep = qtlib.LabeledSeparator(_('Merge to (working directory)')) self.layout().addWidget(local_sep) localCsInfo = create(str(repo['.'].rev())) 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) ### discard option discard_chk = QCheckBox(_('Discard all changes from the other ' 'revision')) self.registerField('discard', discard_chk) self.layout().addWidget(discard_chk) ## auto-resolve autoresolve_chk = QCheckBox(_('Automatically resolve merge conflicts ' 'where possible')) self.registerField('autoresolve', autoresolve_chk) self.layout().addWidget(autoresolve_chk) self.groups.set_visible(False, 'dirty') self.groups.set_visible(False, 'merged') def isComplete(self): 'should Next button be sensitive?' return self._wctxcleaner.isClean() or self.field('force') def validatePage(self): 'validate that we can continue with the merge' if self.field('discard'): 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 repositoryChanged(self): 'repository has detected a change to changelog or parents' pctx = self.repo['.'] self.localCsInfo.update(pctx) 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(str) def onLinkActivated(self, cmd): if cmd == 'skip': self.wizard().next() else: self._wctxcleaner.runCleaner(cmd) class MergePage(BasePage): def __init__(self, repoagent, otherrev, parent): super(MergePage, self).__init__(repoagent, parent) self._otherrev = otherrev self.mergecomplete = False self.setTitle(_('Merging...')) self.setSubTitle(_('All conflicting files will be marked unresolved.')) self.setLayout(QVBoxLayout()) self._cmdsession = cmdcore.nullCmdSession() self._cmdlog = cmdui.LogWidget(self) self.layout().addWidget(self._cmdlog) self.reslabel = QLabel() self.reslabel.linkActivated.connect(self.onLinkActivated) self.reslabel.setWordWrap(True) self.layout().addWidget(self.reslabel) autonext = QCheckBox(_('Automatically advance to next page ' 'when merge is complete.')) autonext.clicked.connect(self.tryAutoAdvance) self.registerField('autoadvance', autonext) self.layout().addWidget(autonext) def currentPage(self): super(MergePage, self).currentPage() if len(self.repo[None].parents()) > 1: self.mergecomplete = True self.completeChanged.emit() return discard = self.field('discard') rev = hglib.tounicode(self._otherrev) cfgs = [] extrakwargs = {} if discard: extrakwargs['tool'] = ':local' # disable changed/deleted prompt because we'll revert changes cfgs.append('ui.interactive=False') elif self.field('autoresolve'): cfgs.append('ui.merge=:merge') else: extrakwargs['tool'] = ':fail' cmdlines = [hglib.buildcmdargs('merge', rev, verbose=True, force=self.field('force'), config=cfgs, **extrakwargs)] if discard: # revert files added/removed at other side cmdlines.append(hglib.buildcmdargs('revert', rev='.', all=True)) self._cmdlog.clearLog() self._cmdsession = sess = self._repoagent.runCommandSequence(cmdlines, self) sess.commandFinished.connect(self.onCommandFinished) sess.outputReceived.connect(self._cmdlog.appendLog) def isComplete(self): 'should Next button be sensitive?' if not self.mergecomplete: return False ucount = 0 rcount = 0 for root, path, status in thgrepo.recursiveMergeStatus(self.repo): if status == 'u': ucount += 1 if status == 'r': rcount += 1 if ucount: if self.field('autoresolve'): # if autoresolve is enabled, we know these were real conflicts self.reslabel.setText(_('%d files have merge conflicts ' 'that must be ' 'resolved') % ucount) else: # else give a calmer indication of conflicts self.reslabel.setText(_('%d files were modified on both ' 'branches and must be ' 'resolved') % ucount) return False elif rcount: self.reslabel.setText(_('No merge conflicts, ready to commit or ' 'review')) else: self.reslabel.setText(_('No merge conflicts, ready to commit')) return True @pyqtSlot(bool) def tryAutoAdvance(self, checked): if checked and self.isComplete(): self.wizard().next() @pyqtSlot(int) def onCommandFinished(self, ret): sess = self._cmdsession if ret in (0, 1): self.mergecomplete = True if self.field('autoadvance') and not sess.warningString(): self.tryAutoAdvance(True) self.completeChanged.emit() @pyqtSlot(str) def onLinkActivated(self, cmd): if cmd == 'resolve': dlg = resolve.ResolveDialog(self._repoagent, self) dlg.exec_() if self.field('autoadvance'): 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._cmdsession = cmdcore.nullCmdSession() self._cmdlog = cmdui.LogWidget(self) self._cmdlog.hide() self.layout().addWidget(self._cmdlog) 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) skiplast = QCheckBox(_('Skip final confirmation page, ' 'close after commit.')) self.registerField('skiplast', skiplast) self.layout().addWidget(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() 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 self.msgEntry.setText(commit.mergecommitmessage(self.repo)) self.msgEntry.moveCursorToEnd() @pyqtSlot(str) def onLinkActivated(self, cmd): if cmd == 'view': dlg = status.StatusDialog(self._repoagent, [], {}, self) dlg.exec_() self.refresh() def isComplete(self): return (len(self.repo[None].parents()) == 2 and len(self.msgEntry.text()) > 0) def validatePage(self): if not self._cmdsession.isFinished(): return False if len(self.repo[None].parents()) == 1: # commit succeeded, repositoryChanged() called wizard().next() if self.field('skiplast'): self.wizard().close() return True user = hglib.tounicode(qtlib.getCurrentUsername(self, self.repo, self.opts)) if not user: return False self.setTitle(_('Committing...')) self.setSubTitle(_('Please wait while committing merged files.')) opts = {'verbose': True, 'message': self.msgEntry.text(), 'user': user, 'subrepos': bool(self.opts.get('recurseinsubrepos')), 'date': hglib.tounicode(self.opts.get('date')), } commandlines = [hglib.buildcmdargs('commit', **opts)] pushafter = self.repo.ui.config('tortoisehg', 'cipushafter') if pushafter: cmd = ['push', hglib.tounicode(pushafter)] commandlines.append(cmd) self._cmdlog.show() sess = self._repoagent.runCommandSequence(commandlines, self) self._cmdsession = sess sess.commandFinished.connect(self.onCommandFinished) sess.outputReceived.connect(self._cmdlog.appendLog) return False def repositoryChanged(self): 'repository has detected a change to changelog or parents' if len(self.repo[None].parents()) == 1: if not self._cmdsession.isFinished(): # call self.wizard().next() after the current command finishes self.delayednext = True else: self.wizard().next() @pyqtSlot() def onCommandFinished(self): 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 = qtlib.readStringList(s, 'commit/userhist') userhist = [u for u in userhist if u] return userhist def details(self): self.userhist = self.readUserHistory() dlg = commit.DetailsDialog(self._repoagent, 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-4.5.2/tortoisehg/hgqt/htmlui.py0000644000175000017500000000616613150123225021534 0ustar sborhosborho00000000000000# 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 cgi, time from mercurial import ui from tortoisehg.hgqt import qtlib 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.load() repo = hg.repository(u) repo.status() print u.getdata()[0] tortoisehg-4.5.2/tortoisehg/hgqt/repotreeitem.py0000644000175000017500000004777113150123225022745 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import re from .qtcore import ( Qt, ) from .qtgui import ( QApplication, QMessageBox, QStyle, ) from mercurial import ( error, hg, node, util, ) from ..util import ( hglib, paths, ) from ..util.i18n import _ from . import ( hgrcutil, qtlib, ) def _dumpChild(xw, parent): for c in parent.childs: c.dumpObject(xw) def undumpObject(xr): xmltagname = str(xr.name()) 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') # '/' for path separator, '#n' for index of duplicated names _quotenamere = re.compile(r'[%/#]') def _quotename(s): r"""Replace special characters to %xx (minimal set of urllib.quote) >>> _quotename('foo/bar%baz#qux') 'foo%2Fbar%25baz%23qux' >>> _quotename(u'\xa1') u'\xa1' """ return _quotenamere.sub(lambda m: '%%%02X' % ord(m.group(0)), s) def _buildquotenamemap(items): namemap = {} for e in items: q = _quotename(e.shortname()) if q not in namemap: namemap[q] = [e] else: namemap[q].append(e) return namemap def itempath(item): """Virtual path to the given item""" rnames = [] while item.parent(): namemap = _buildquotenamemap(item.parent().childs) q = _quotename(item.shortname()) i = namemap[q].index(item) if i == 0: rnames.append(q) else: rnames.append('%s#%d' % (q, i)) item = item.parent() return '/'.join(reversed(rnames)) def findbyitempath(root, path): """Return the item for the given virtual path >>> root = RepoTreeItem() >>> foo = RepoGroupItem('foo') >>> root.appendChild(foo) >>> bar = RepoGroupItem('bar') >>> root.appendChild(bar) >>> bar.appendChild(RepoItem('/tmp/baz', 'baz')) >>> root.appendChild(RepoGroupItem('foo')) >>> root.appendChild(RepoGroupItem('qux/quux')) >>> def f(path): ... return itempath(findbyitempath(root, path)) >>> f('') '' >>> f('foo') 'foo' >>> f('bar/baz') 'bar/baz' >>> f('qux%2Fquux') 'qux%2Fquux' >>> f('bar/baz/unknown') Traceback (most recent call last): ... ValueError: not found >>> f('foo#1') 'foo#1' >>> f('foo#2') Traceback (most recent call last): ... ValueError: not found >>> f('foo#bar') Traceback (most recent call last): ... ValueError: invalid path """ if not path: return root item = root for q in path.split('/'): h = q.rfind('#') if h >= 0: try: i = int(q[h + 1:]) except ValueError: raise ValueError('invalid path') q = q[:h] else: i = 0 namemap = _buildquotenamemap(item.childs) try: item = namemap[q][i] except LookupError: raise ValueError('not found') return item 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 None 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, sharedpath=None, parent=None): RepoTreeItem.__init__(self, parent) self._root = root self._shortname = shortname or u'' self._basenode = basenode or node.nullid # expensive check is done at appendSubrepos() self._sharedpath = sharedpath or '' self._valid = True def isRepo(self): return True def rootpath(self): return self._root def shortname(self): if self._shortname: return self._shortname else: return 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')) elif self._sharedpath: ico = qtlib.getoverlaidicon(ico, qtlib.geticon('hg-sharedrepo')) 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 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', self._root) xw.writeAttribute('shortname', self.shortname()) xw.writeAttribute('basenode', node.hex(self.basenode())) if self._sharedpath: xw.writeAttribute('sharedpath', self._sharedpath) _dumpChild(xw, parent=self) @classmethod def undump(cls, xr): a = xr.attributes() obj = cls(unicode(a.value('', 'root')), unicode(a.value('', 'shortname')), node.bin(str(a.value('', 'basenode'))), unicode(a.value('', 'sharedpath'))) _undumpChild(xr, parent=obj, undump=_undumpSubrepoItem) return obj def details(self): return _('Local Repository %s') % self._root def appendSubrepos(self, repo=None): self._sharedpath = '' invalidRepoList = [] try: sri = None if repo is None: if not os.path.exists(self._root): self._valid = False return [hglib.fromunicode(self._root)] elif (not os.path.exists(os.path.join(self._root, '.hgsub')) and not os.path.exists( os.path.join(self._root, '.hg', 'sharedpath'))): return [] # skip repo creation, which is expensive repo = hg.repository(hglib.loadui(), hglib.fromunicode(self._root)) if repo.sharedpath != repo.path: self._sharedpath = hglib.tounicode(repo.sharedpath) 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(hglib.tounicode(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(hglib.fromunicode(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(hglib.fromunicode(self._root)) # Show a warning message indicating that there was an error if repo: rootpath = hglib.tounicode(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.')) \ % (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) abshgrcpath = os.path.join(hglib.fromunicode(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) 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', self._root) xw.writeAttribute('repotype', self._repotype) @classmethod def undump(cls, xr): a = xr.attributes() obj = cls(unicode(a.value('', 'root')), str(a.value('', 'repotype'))) 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')) 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 ico return None if column == 0: return self.name elif column == 1: return self.getCommonPath() return None def setData(self, column, value): if column == 0: self.name = unicode(value) return True return False def rootpath(self): # for sortbypath() return '' # may be okay to return _commonpath instead? def shortname(self): # for sortbyname() return 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(unicode(a.value('', 'name'))) _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 cpath is not 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)) 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-4.5.2/tortoisehg/hgqt/revdetails.py0000644000175000017500000005603413153775104022407 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os from .qtcore import ( QEvent, QPoint, QSettings, QSize, QTimer, QUrl, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAction, QActionGroup, QDialog, QFrame, QKeySequence, QLayout, QLineEdit, QMenu, QSizePolicy, QSplitter, QStyleFactory, QTextBrowser, QTextEdit, QToolBar, QToolButton, QVBoxLayout, QWidget, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdui, filectxactions, manifestmodel, qtlib, status, ) from .filelistview import HgFileListView from .fileview import HgFileView from .revpanel import RevPanelWidget _fileactionsbytype = { 'subrepo': ['openSubrepo', 'explore', 'terminal', 'copyPath', None, 'revertFile'], 'file': ['visualDiffFile', 'visualDiffFileToLocal', None, 'editFile', 'saveFile', None, 'editLocalFile', 'openLocalFile', 'exploreLocalFile', 'copyPath', None, 'revertFile', None, 'navigateFileLog', 'navigateFileDiff', 'filterFile'], 'dir': ['visualDiffFile', 'visualDiffFileToLocal', None, 'revertFile', None, 'filterFile', None, 'explore', 'terminal', 'copyPath'], } class RevDetailsWidget(QWidget, qtlib.TaskWidget): showMessage = pyqtSignal(str) linkActivated = pyqtSignal(str) grepRequested = pyqtSignal(str, dict) revisionSelected = pyqtSignal(int) revsetFilterRequested = pyqtSignal(str) runCustomCommandRequested = pyqtSignal(str, list) def __init__(self, repoagent, parent, rev=None): QWidget.__init__(self, parent) self._repoagent = repoagent repo = repoagent.rawRepo() self.ctx = repo[rev] self.splitternames = [] self.setupUi() self.createActions() self.setupModels() self.filelist.installEventFilter(self) self.filefilter.installEventFilter(self) self._deschtmlize = qtlib.descriptionhtmlizer(repo.ui) repoagent.configChanged.connect(self._updatedeschtmlizer) @property def repo(self): return self._repoagent.rawRepo() def setupUi(self): self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) # + basevbox -------------------------------------------------------+ # |+ filelistsplit ........ | # | + filelistframe (vbox) | + panelframe (vbox) | # | + filelisttbar | + revpanel | # +---------------------------+-------------------------------------+ # | + filelist | + messagesplitter | # | | :+ message | # | | :----------------------------------+ # | | + fileview | # +---------------------------+-------------------------------------+ basevbox = QVBoxLayout(self) basevbox.setSpacing(0) basevbox.setContentsMargins(2, 2, 2, 2) self.filelistsplit = QSplitter(self) basevbox.addWidget(self.filelistsplit) self.splitternames.append('filelistsplit') self.filelistsplit.setOrientation(Qt.Horizontal) self.filelistsplit.setChildrenCollapsible(False) self.filelisttbar = QToolBar(_('File List Toolbar')) self.filelisttbar.setIconSize(qtlib.smallIconSize()) self.filelist = HgFileListView(self) self.filelist.setContextMenuPolicy(Qt.CustomContextMenu) self.filelist.customContextMenuRequested.connect(self.menuRequest) self.filelist.doubleClicked.connect(self.onDoubleClick) self._filelistpaletteswitcher = qtlib.PaletteSwitcher(self.filelist) self.filelistframe = QWidget(self.filelistsplit) self.filelistsplit.setStretchFactor(0, 3) vbox = QVBoxLayout() vbox.setSpacing(0) vbox.setContentsMargins(0, 0, 0, 0) vbox.addWidget(self.filelisttbar) vbox.addWidget(self.filelist) self.filelistframe.setLayout(vbox) self.fileviewframe = QWidget(self.filelistsplit) self.filelistsplit.setStretchFactor(1, 7) vbox = QVBoxLayout(self.fileviewframe) vbox.setSpacing(0) vbox.setSizeConstraint(QLayout.SetDefaultConstraint) vbox.setContentsMargins(0, 0, 0, 0) panelframevbox = vbox self.messagesplitter = QSplitter(self.fileviewframe) if os.name == 'nt': self.messagesplitter.setStyle(QStyleFactory.create('Plastique')) self.splitternames.append('messagesplitter') self.messagesplitter.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) 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) 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) self.messagesplitter.setStretchFactor(1, 1) self.fileview.setMinimumSize(QSize(0, 0)) self.fileview.linkActivated.connect(self.linkActivated) self.fileview.showMessage.connect(self.showMessage) self.fileview.grepRequested.connect(self.grepRequested) self.fileview.revisionSelected.connect(self.revisionSelected) self.filelist.fileSelected.connect(self._onFileSelected) self.filelist.clearDisplay.connect(self._onFileSelected) 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): model = manifestmodel.ManifestModel(self._repoagent, self) model.setFlat(not self.isManifestMode() and self.isFlatFileList()) model.setStatusFilter(self.fileStatusFilter()) model.revLoaded.connect(self._expandShortFileList) self.filelist.setModel(model) # fileSelected is actually the wrapper of currentChanged, which is # unrelated to the selection self.filelist.selectionModel().selectionChanged.connect( self.updateItemFileActions) def createActions(self): self._createFileListActions() self._parentToggleGroup.actions()[0].setChecked(True) self._fileactions = filectxactions.FilectxActions(self._repoagent, self) self._fileactions.setupCustomToolsMenu('workbench.filelist.custom-menu') self._fileactions.linkActivated.connect(self.linkActivated) self._fileactions.filterRequested.connect(self.revsetFilterRequested) self._fileactions.runCustomCommandRequested.connect( self.runCustomCommandRequested) self.addActions(self._fileactions.actions()) def _createFileListActions(self): tbar = self.filelisttbar self._actionManifestMode = a = tbar.addAction(_('Ma&nifest Mode')) a.setCheckable(True) a.setIcon(qtlib.geticon('hg-annotate')) a.setToolTip(_('Show all version-controlled files in tree view')) a.triggered.connect(self._applyManifestMode) self._actionFlatFileList = a = QAction(_('&Flat List'), self) a.setCheckable(True) a.setChecked(True) a.triggered.connect(self._applyFlatFileList) le = QLineEdit() if hasattr(le, 'setPlaceholderText'): # Qt >= 4.7 le.setPlaceholderText(_('### filter text ###')) self.filefilter = le tbar.addWidget(self.filefilter) t = QTimer(self, interval=200, singleShot=True) t.timeout.connect(self._applyFileNameFilter) le.textEdited.connect(t.start) le.returnPressed.connect(self.filelist.expandAll) w = status.StatusFilterActionGroup('MARS', 'MARCS', self) self._fileStatusFilter = w w.statusChanged.connect(self._applyFileStatusFilter) # TODO: p1/p2 toggle should be merged with fileview's self._parentToggleGroup = QActionGroup(self) self._parentToggleGroup.triggered.connect(self._selectParentRevision) for i, (icon, text, tip) in enumerate([ ('hg-merged-both', _('Changed by &This Commit'), _('Show files changed by this commit')), ('hg-merged-p1', _('Compare to &1st Parent'), _('Show changes from first parent')), ('hg-merged-p2', _('Compare to &2nd Parent'), _('Show changes from second parent'))]): a = self._parentToggleGroup.addAction(qtlib.geticon(icon), text) a.setCheckable(True) a.setData(i) a.setStatusTip(tip) w = QToolButton(self) m = QMenu(w) m.addActions(self._parentToggleGroup.actions()) w.setMenu(m) w.setPopupMode(QToolButton.MenuButtonPopup) self._actionParentToggle = a = tbar.addWidget(w) a.setIcon(qtlib.geticon('hg-merged-both')) a.setToolTip(_('Toggle parent to be used as the base revision')) a.triggered.connect(self._toggleParentRevision) w.setDefaultAction(a) def canswitch(self): # assumes a user wants to browse changesets in manifest mode. commit # widget isn't suitable for such usage. return not self.isManifestMode() def eventFilter(self, watched, event): # switch between filter and list seamlessly if watched is self.filefilter: if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Down: self.filelist.setFocus() return True return False elif watched is self.filelist: if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Up: index = self.filelist.currentIndex() if index.row() == 0 and not index.parent().isValid(): self.filefilter.setFocus() return True return False return super(RevDetailsWidget, self).eventFilter(watched, event) 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._setContextToFileList(ctx) def _setContextToFileList(self, ctx): # useless to toggle manifest mode in patchctx self._actionManifestMode.setEnabled(not ctx.thgmqunappliedpatch()) self._parentToggleGroup.setVisible(len(ctx.parents()) == 2) self._actionParentToggle.setVisible(self._parentToggleGroup.isVisible()) m = self.filelist.model() if len(ctx.parents()) != 2: m.setRawContext(ctx) m.setChangedFilesOnly(False) self.updateItemFileActions() return parentmode = self._parentToggleGroup.checkedAction().data() pnum, changedonly = [(0, True), (0, False), (1, False)][parentmode] m.setRev(ctx.rev(), ctx.parents()[pnum].rev()) m.setChangedFilesOnly(changedonly) self.updateItemFileActions() @pyqtSlot(QAction) def _selectParentRevision(self, action): self._actionParentToggle.setIcon(action.icon()) self._setContextToFileList(self.ctx) @pyqtSlot() def _toggleParentRevision(self): parentactions = [a for a in self._parentToggleGroup.actions() if a.isEnabled()] i = parentactions.index(self._parentToggleGroup.checkedAction()) parentactions[(i + 1) % len(parentactions)].trigger() @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(QModelIndex) def onDoubleClick(self, index): model = self.filelist.model() if model.subrepoType(index): self._fileactions.openSubrepo() elif model.isDir(index): # expand/collapse tree by default pass elif model.fileStatus(index) == 'C': self._fileactions.editFile() else: self._fileactions.visualDiffFile() def filePath(self): return hglib.tounicode(self.filelist.currentFile()) def setFilePath(self, path): self.filelist.setCurrentFile(hglib.fromunicode(path)) def showLine(self, line): self.fileview.showLine(line - 1) # fileview should do -1 instead? def setSearchPattern(self, text): self.fileview.searchbar.setPattern(text) @pyqtSlot() def _onFileSelected(self): index = self.filelist.currentIndex() model = self.filelist.model() self.fileview.display(model.fileData(index)) @pyqtSlot(QPoint) def menuRequest(self, point): contextmenu = QMenu(self) if self.filelist.selectionModel().hasSelection(): self._setupFileMenu(contextmenu) contextmenu.addSeparator() m = contextmenu.addMenu(_('List Optio&ns')) else: m = contextmenu m.addAction(self._actionManifestMode) m.addSeparator() m.addActions(self._fileStatusFilter.actions()) m.addSeparator() m.addActions(self._parentToggleGroup.actions()) m.addSeparator() m.addAction(self._actionFlatFileList) contextmenu.setAttribute(Qt.WA_DeleteOnClose) contextmenu.popup(self.filelist.viewport().mapToGlobal(point)) def _setupFileMenu(self, contextmenu): index = self.filelist.currentIndex() model = self.filelist.model() # Subrepos and regular items have different context menus if model.subrepoType(index): actnames = _fileactionsbytype['subrepo'] elif model.isDir(index): actnames = _fileactionsbytype['dir'] else: actnames = _fileactionsbytype['file'] for act in actnames + [None, 'customToolsMenu']: if act: contextmenu.addAction(self._fileactions.action(act)) else: contextmenu.addSeparator() @pyqtSlot() def updateItemFileActions(self): model = self.filelist.model() selmodel = self.filelist.selectionModel() selfds = map(model.fileData, selmodel.selectedIndexes()) self._fileactions.setFileDataList(selfds) @pyqtSlot() def _applyFileNameFilter(self): model = self.filelist.model() match = self.filefilter.text() if model is not None: model.setNameFilter(match) self._filelistpaletteswitcher.enablefilterpalette(bool(match)) self._expandShortFileList() def isManifestMode(self): """In manifest mode, clean files are listed and removed are hidden by default. Also, the view is forcibly switched to the tree mode.""" return self._actionManifestMode.isChecked() def setManifestMode(self, manifestmode): self._actionManifestMode.setChecked(manifestmode) self._applyManifestMode(manifestmode) @pyqtSlot(bool) def _applyManifestMode(self, manifestmode): self._fileStatusFilter.setChecked('C', manifestmode) self._fileStatusFilter.setChecked('R', not manifestmode) self._actionFlatFileList.setVisible(not manifestmode) self._applyFlatFileList(not manifestmode and self.isFlatFileList()) # manifest should show clean files, so only p1/p2 toggles are valid parentactions = self._parentToggleGroup.actions() parentactions[0].setEnabled(not manifestmode) parentactions[int(manifestmode)].trigger() def isFlatFileList(self): return self._actionFlatFileList.isChecked() def setFlatFileList(self, flat): self._actionFlatFileList.setChecked(flat) if not self.isManifestMode(): self._applyFlatFileList(flat) @pyqtSlot(bool) def _applyFlatFileList(self, flat): view = self.filelist model = view.model() model.setFlat(flat) view.setRootIsDecorated(not flat) if flat: view.setTextElideMode(Qt.ElideLeft) else: view.setTextElideMode(Qt.ElideRight) self._expandShortFileList() def fileStatusFilter(self): return self._fileStatusFilter.status() def setFileStatusFilter(self, statustext): self._fileStatusFilter.setStatus(statustext) @pyqtSlot(str) def _applyFileStatusFilter(self, statustext): model = self.filelist.model() model.setStatusFilter(statustext) self._expandShortFileList() @pyqtSlot() def _expandShortFileList(self): if self.isManifestMode(): # because manifest will contain large tree of files return self.filelist.expandAll() def saveSettings(self, s): wb = "RevDetailsWidget/" for n in self.splitternames: s.setValue(wb + n, getattr(self, n).saveState()) s.setValue(wb + 'flatfilelist', self.isFlatFileList()) 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(qtlib.readByteArray(s, wb + n)) self.setFlatFileList(qtlib.readBool(s, wb + 'flatfilelist', True)) expanded = qtlib.readBool(s, wb + 'revpanel.expanded', False) 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')) self._repoagent = repoagent layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 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(qtlib.readByteArray(s, 'revdetails/geom')) 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 filePath(self): return self.revdetails.filePath() def setFilePath(self, path): self.revdetails.setFilePath(path) def showLine(self, line): self.revdetails.showLine(line) def setSearchPattern(self, text): self.revdetails.setSearchPattern(text) def isManifestMode(self): return self.revdetails.isManifestMode() def setManifestMode(self, manifestmode): self.revdetails.setManifestMode(manifestmode) def isFlatFileList(self): return self.revdetails.isFlatFileList() def setFlatFileList(self, flat): self.revdetails.setFlatFileList(flat) def fileStatusFilter(self): return self.revdetails.fileStatusFilter() def setFileStatusFilter(self, statustext): self.revdetails.setFileStatusFilter(statustext) 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._repoagent.displayName(), revstr)) self.revdetails.reload() def done(self, ret): s = QSettings() s.setValue('revdetails/geom', self.saveGeometry()) super(RevDetailsDialog, self).done(ret) def createManifestDialog(repoagent, rev=None, parent=None): dlg = RevDetailsDialog(repoagent, rev, parent) dlg.setManifestMode(True) return dlg tortoisehg-4.5.2/tortoisehg/hgqt/webconf_ui.py0000644000175000017500000000764213242104460022354 0ustar sborhosborho00000000000000# -*- coding: utf-8 -*- # Form implementation generated from reading ui file '/home/sborho/repos/thg/tortoisehg/hgqt/webconf.ui' # # Created by: PyQt5 UI code generator 5.5.1 # # WARNING! All changes made in this file will be lost! from tortoisehg.util.i18n import _ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_WebconfForm(object): def setupUi(self, WebconfForm): WebconfForm.setObjectName("WebconfForm") WebconfForm.resize(455, 300) self.form_layout = QtWidgets.QVBoxLayout(WebconfForm) self.form_layout.setObjectName("form_layout") self.path_layout = QtWidgets.QHBoxLayout() self.path_layout.setObjectName("path_layout") self.path_label = QtWidgets.QLabel(WebconfForm) self.path_label.setObjectName("path_label") self.path_layout.addWidget(self.path_label) self.path_edit = QtWidgets.QComboBox(WebconfForm) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.path_edit.sizePolicy().hasHeightForWidth()) self.path_edit.setSizePolicy(sizePolicy) self.path_edit.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop) self.path_edit.setObjectName("path_edit") self.path_layout.addWidget(self.path_edit) self.open_button = QtWidgets.QToolButton(WebconfForm) self.open_button.setObjectName("open_button") self.path_layout.addWidget(self.open_button) self.save_button = QtWidgets.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 = QtWidgets.QFrame(WebconfForm) self.filerepos_sep.setFrameShape(QtWidgets.QFrame.HLine) self.filerepos_sep.setFrameShadow(QtWidgets.QFrame.Sunken) self.filerepos_sep.setObjectName("filerepos_sep") self.form_layout.addWidget(self.filerepos_sep) self.repos_layout = QtWidgets.QHBoxLayout() self.repos_layout.setObjectName("repos_layout") self.repos_view = QtWidgets.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 = QtWidgets.QVBoxLayout() self.addremove_layout.setObjectName("addremove_layout") self.add_button = QtWidgets.QToolButton(WebconfForm) self.add_button.setObjectName("add_button") self.addremove_layout.addWidget(self.add_button) self.edit_button = QtWidgets.QToolButton(WebconfForm) self.edit_button.setObjectName("edit_button") self.addremove_layout.addWidget(self.edit_button) self.remove_button = QtWidgets.QToolButton(WebconfForm) self.remove_button.setObjectName("remove_button") self.addremove_layout.addWidget(self.remove_button) spacerItem = QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.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): _translate = QtCore.QCoreApplication.translate 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-4.5.2/tortoisehg/hgqt/filectxactions.py0000644000175000017500000006725213150123225023254 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import re from .qtcore import ( QMimeData, QObject, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAction, QApplication, QFileDialog, QMenu, QWidget, ) from ..util import ( hglib, shlib, ) from ..util.i18n import _ from . import ( cmdcore, cmdui, customtools, lfprompt, qtlib, rejects, revert, visdiff, ) def _lcanonpaths(fds): return [hglib.fromunicode(e.canonicalFilePath()) for e in fds] # predicates to filter files def _anydeleted(fds): if any(e.rev() is None and e.rawContext().deleted() for e in fds): return fds return [] def _committed(fds): return [e for e in fds if e.rev() is not None and e.rev() >= 0] def _filepath(pat): patre = re.compile(pat) return lambda fds: [e for e in fds if patre.search(e.filePath())] def _filestatus(s): s = frozenset(s) # include directory since its status is unknown return lambda fds: [e for e in fds if e.isDir() or e.fileStatus() in s] def _indirectbaserev(fds): return [e for e in fds if e.baseRev() not in e.parentRevs()] def _isdir(fds): return [e for e in fds if e.isDir()] def _isfile(fds): return [e for e in fds if not e.isDir()] def _merged(fds): return [e for e in fds if len(e.rawContext().parents()) > 1] def _mergestatus(s): s = frozenset(s) # include directory since its status is unknown return lambda fds: [e for e in fds if e.isDir() or e.mergeStatus() in s] def _notpatch(fds): return [e for e in fds if e.rev() is None or e.rev() >= 0] def _notsubrepo(fds): return [e for e in fds if not e.repoRootPath() and not e.subrepoType()] def _notsubroot(fds): return [e for e in fds if not e.subrepoType()] def _single(fds): if len(fds) != 1: return [] return fds def _subrepotype(t): return lambda fds: [e for e in fds if e.subrepoType() == t] def _filterby(fdfilters, fds): for f in fdfilters: if not fds: return [] fds = f(fds) return fds def _tablebuilder(table): """Make decorator to define actions that receive filtered files If the slot, wrapped(), is invoked, the specified function is called with filtered files, func(fds), only if "fds" is not empty. """ def slot(text, icon, shortcut, statustip, fdfilters=()): if not isinstance(fdfilters, tuple): fdfilters = (fdfilters,) def decorate(func): name = func.__name__ table[name] = (text, icon, shortcut, statustip, fdfilters) def wrapped(self): fds = self.fileDataListForAction(name) if not fds: return func(self, fds) return pyqtSlot(name=name)(wrapped) return decorate return slot class FilectxActions(QObject): """Container for repository file actions""" linkActivated = pyqtSignal(str) filterRequested = pyqtSignal(str) """Ask the repowidget to change its revset filter""" runCustomCommandRequested = pyqtSignal(str, list) _actiontable = {} actionSlot = _tablebuilder(_actiontable) def __init__(self, repoagent, parent): super(FilectxActions, self).__init__(parent) if not isinstance(parent, QWidget): raise ValueError('parent must be a QWidget') self._repoagent = repoagent self._cmdsession = cmdcore.nullCmdSession() self._selfds = [] self._nav_dialogs = qtlib.DialogKeeper(FilectxActions._createnavdialog, FilectxActions._gennavdialogkey, self) self._actions = {} self._customactions = {} for name, d in self._actiontable.iteritems(): desc, icon, key, tip, fdfilters = d # QAction must be owned by QWidget; otherwise statusTip for context # menu cannot be displayed (QTBUG-16114) act = QAction(desc, self.parent()) if icon: act.setIcon(qtlib.geticon(icon)) if key: act.setShortcut(key) act.setShortcutContext(Qt.WidgetWithChildrenShortcut) if tip: act.setStatusTip(tip) act.triggered.connect(getattr(self, name)) self._addAction(name, act, fdfilters) self._initAdditionalActions() self._updateActions() def _initAdditionalActions(self): # override to add actions that cannot be declared as actionSlot pass @property def _ui(self): repo = self._repoagent.rawRepo() return repo.ui def _repoAgentFor(self, fd): rpath = fd.repoRootPath() if not rpath: return self._repoagent return self._repoagent.subRepoAgent(rpath) def _updateActions(self): idle = self._cmdsession.isFinished() selfds = self._selfds allactions = self._actions.values() + self._customactions.values() for act, fdfilters in allactions: act.setEnabled(idle and bool(_filterby(fdfilters, selfds))) def fileDataListForAction(self, name): fdfilters = self._actions[name][1] return _filterby(fdfilters, self._selfds) def setFileDataList(self, selfds): self._selfds = list(selfds) self._updateActions() def actions(self): """List of the actions; The owner widget should register them""" return [a for a, _f in self._actions.itervalues()] def action(self, name): return self._actions[name][0] def _addAction(self, name, action, fdfilters): assert name not in self._actions self._actions[name] = action, fdfilters def _runCommand(self, cmdline): if not self._cmdsession.isFinished(): return cmdcore.nullCmdSession() sess = self._repoagent.runCommand(cmdline, self) self._handleNewCommand(sess) return sess def _runCommandSequence(self, cmdlines): if not self._cmdsession.isFinished(): return cmdcore.nullCmdSession() sess = self._repoagent.runCommandSequence(cmdlines, self) self._handleNewCommand(sess) return sess def _handleNewCommand(self, sess): assert self._cmdsession.isFinished() self._cmdsession = sess sess.commandFinished.connect(self._onCommandFinished) self._updateActions() @pyqtSlot(int) def _onCommandFinished(self, ret): if ret == 255: cmdui.errorMessageBox(self._cmdsession, self.parent()) self._updateActions() @actionSlot(_('File &History / Annotate'), 'hg-log', 'Shift+Return', _('Show the history of the selected file'), (_isfile, _notpatch, _filestatus('MARC!'))) def navigateFileLog(self, fds): from tortoisehg.hgqt import filedialogs, fileview for fd in fds: dlg = self._navigate(filedialogs.FileLogDialog, fd) if not dlg: continue dlg.setFileViewMode(fileview.AnnMode) @actionSlot(_('Co&mpare File Revisions'), 'compare-files', None, _('Compare revisions of the selected file'), (_isfile, _notpatch)) def navigateFileDiff(self, fds): from tortoisehg.hgqt import filedialogs for fd in fds: self._navigate(filedialogs.FileDiffDialog, fd) def _navigate(self, dlgclass, fd): repoagent = self._repoAgentFor(fd) repo = repoagent.rawRepo() filename = hglib.fromunicode(fd.canonicalFilePath()) if repo.file(filename): dlg = self._nav_dialogs.open(dlgclass, repoagent, filename) dlg.goto(fd.rev()) return dlg def _createnavdialog(self, dlgclass, repoagent, filename): return dlgclass(repoagent, filename) def _gennavdialogkey(self, dlgclass, repoagent, filename): repo = repoagent.rawRepo() return dlgclass, repo.wjoin(filename) @actionSlot(_('Filter Histor&y'), 'hg-log', None, _('Query about changesets affecting the selected files'), _notsubrepo) def filterFile(self, fds): pats = ["file('path:%s')" % e.filePath() for e in fds] self.filterRequested.emit(' or '.join(pats)) @actionSlot(_('Diff &Changeset to Parent'), 'visualdiff', None, '', _notpatch) def visualDiff(self, fds): self._visualDiffToBase(fds, []) @actionSlot(_('Diff Changeset to Loc&al'), 'ldiff', None, '', _committed) def visualDiffToLocal(self, fds): self._visualDiff(fds, [], rev=['rev(%d)' % fds[0].rev()]) @actionSlot(_('&Diff to Parent'), 'visualdiff', 'Ctrl+D', _('View file changes in external diff tool'), (_notpatch, _notsubroot, _filestatus('MAR!'))) def visualDiffFile(self, fds): self._visualDiffToBase(fds, _lcanonpaths(fds)) @actionSlot(_('Diff to &Local'), 'ldiff', 'Shift+Ctrl+D', _('View changes to current in external diff tool'), _committed) def visualDiffFileToLocal(self, fds): self._visualDiff(fds, _lcanonpaths(fds), rev=['rev(%d)' % fds[0].rev()]) def _visualDiffToBase(self, fds, filenames): if fds[0].baseRev() == fds[0].parentRevs()[0]: self._visualDiff(fds, filenames, change=fds[0].rev()) # can 3-way else: revs = [fds[0].baseRev()] if fds[0].rev() is not None: revs.append(fds[0].rev()) self._visualDiff(fds, filenames, rev=['rev(%d)' % r for r in revs]) def _visualDiff(self, fds, filenames, **opts): repo = self._repoAgentFor(fds[0]).rawRepo() dlg = visdiff.visualdiff(repo.ui, repo, filenames, opts) if dlg: dlg.exec_() @actionSlot(_('&View at Revision'), 'view-at-revision', 'Shift+Ctrl+E', _('View file as it appeared at this revision'), _committed) def editFile(self, fds): self._editFileAt(fds, fds[0].rawContext()) def _editFileAt(self, fds, ctx): repo = self._repoAgentFor(fds[0]).rawRepo() filenames = _lcanonpaths(fds) base, _ = visdiff.snapshot(repo, filenames, ctx) files = [os.path.join(base, filename) for filename in filenames] qtlib.editfiles(repo, files, parent=self.parent()) @actionSlot(_('&Save at Revision...'), None, 'Shift+Ctrl+S', _('Save file as it appeared at this revision'), _committed) def saveFile(self, fds): cmdlines = [] for fd in fds: wfile, ext = os.path.splitext(fd.absoluteFilePath()) extfilter = [_("All files (*)")] filename = "%s@%d%s" % (wfile, fd.rev(), ext) if ext: extfilter.insert(0, "*%s" % ext) result, _filter = QFileDialog.getSaveFileName( self.parent(), _("Save file to"), filename, ";;".join(extfilter)) if not result: continue # checkout in working-copy line endings, etc. by --decode cmdlines.append(hglib.buildcmdargs( 'cat', hglib.escapepath(fd.canonicalFilePath()), rev=fd.rev(), output=result, decode=True)) if cmdlines: self._runCommandSequence(cmdlines) @actionSlot(_('&Edit Local'), 'edit-file', None, _('Edit current file in working copy'), (_isfile, _filestatus('MACI?'))) def editLocalFile(self, fds): repo = self._repoAgentFor(fds[0]).rawRepo() filenames = _lcanonpaths(fds) qtlib.editfiles(repo, filenames, parent=self.parent()) @actionSlot(_('&Open Local'), None, 'Shift+Ctrl+L', _('Edit current file in working copy'), (_isfile, _filestatus('MACI?'))) def openLocalFile(self, fds): repo = self._repoAgentFor(fds[0]).rawRepo() filenames = _lcanonpaths(fds) qtlib.openfiles(repo, filenames) @actionSlot(_('E&xplore Local'), 'system-file-manager', None, _('Open parent folder of current file in the system file ' 'manager'), (_isfile, _filestatus('MACI?'))) def exploreLocalFile(self, fds): for fd in fds: qtlib.openlocalurl(os.path.dirname(fd.absoluteFilePath())) @actionSlot(_('&Copy Patch'), 'copy-patch', None, '', (_notpatch, _notsubroot, _filestatus('MAR!'))) def copyPatch(self, fds): paths = [hglib.escapepath(fd.filePath()) for fd in fds] revs = map(hglib.escaperev, [fds[0].baseRev(), fds[0].rev()]) cmdline = hglib.buildcmdargs('diff', *paths, r=revs) sess = self._runCommand(cmdline) sess.setCaptureOutput(True) sess.commandFinished.connect(self._copyPatchOutputToClipboard) @pyqtSlot(int) def _copyPatchOutputToClipboard(self, ret): if ret != 0: return output = self._cmdsession.readAll() mdata = QMimeData() mdata.setData('text/x-diff', output) # for lossless import mdata.setText(hglib.tounicode(str(output))) QApplication.clipboard().setMimeData(mdata) @actionSlot(_('Copy &Path'), None, 'Shift+Ctrl+C', _('Copy full path of file(s) to the clipboard')) def copyPath(self, fds): paths = [e.absoluteFilePath() for e in fds] QApplication.clipboard().setText(os.linesep.join(paths)) @actionSlot(_('&Revert to Revision...'), 'hg-revert', 'Shift+Ctrl+R', _('Revert file(s) to contents at this revision'), _notpatch) def revertFile(self, fds): repoagent = self._repoAgentFor(fds[0]) fileSelection = _lcanonpaths(fds) rev = fds[0].rev() if rev is None: repo = repoagent.rawRepo() rev = repo[rev].p1().rev() dlg = revert.RevertDialog(repoagent, fileSelection, rev, parent=self.parent()) dlg.exec_() @actionSlot(_('Open S&ubrepository'), 'thg-repository-open', None, _('Open the selected subrepository'), _subrepotype('hg')) def openSubrepo(self, fds): for fd in fds: if fd.rev() is None: link = 'repo:%s' % fd.absoluteFilePath() else: ctx = fd.rawContext() spath = hglib.fromunicode(fd.canonicalFilePath()) revid = ctx.substate[spath][1] link = 'repo:%s?%s' % (fd.absoluteFilePath(), revid) self.linkActivated.emit(link) @actionSlot(_('E&xplore Folder'), 'system-file-manager', None, _('Open the selected folder in the system file manager'), _isdir) def explore(self, fds): for fd in fds: qtlib.openlocalurl(fd.absoluteFilePath()) @actionSlot(_('Open &Terminal'), 'utilities-terminal', None, _('Open a shell terminal in the selected folder'), _isdir) def terminal(self, fds): for fd in fds: root = hglib.fromunicode(fd.absoluteFilePath()) currentfile = hglib.fromunicode(fd.filePath()) qtlib.openshell(root, currentfile, self._ui) def setupCustomToolsMenu(self, location): tools, toollist = hglib.tortoisehgtools(self._ui, location) submenu = QMenu(_('Custom Tools'), self.parent()) submenu.triggered.connect(self._runCustomCommandByMenu) 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', customtools.DEFAULTICONNAME) status = info.get('status') a = submenu.addAction(label) a.setData(name) if icon: a.setIcon(qtlib.geticon(icon)) if status: fdfilters = (_filestatus(status),) else: fdfilters = () self._customactions[name] = (a, fdfilters) submenu.menuAction().setVisible(bool(self._customactions)) self._addAction('customToolsMenu', submenu.menuAction(), ()) self._updateActions() @pyqtSlot(QAction) def _runCustomCommandByMenu(self, action): name = str(action.data()) fdfilters = self._customactions[name][1] fds = _filterby(fdfilters, self._selfds) files = [hglib.fromunicode(fd.filePath()) for fd in fds] self.runCustomCommandRequested.emit(name, files) class WctxActions(FilectxActions): 'container class for working context actions' refreshNeeded = pyqtSignal() _actiontable = FilectxActions._actiontable.copy() actionSlot = _tablebuilder(_actiontable) def _initAdditionalActions(self): repo = self._repoagent.rawRepo() # the same shortcut as editFile that is disabled for working rev a = self.action('editLocalFile') a.setShortcut('Ctrl+Shift+E') a.setShortcutContext(Qt.WidgetWithChildrenShortcut) a = self.action('addLargefile') a.setVisible('largefiles' in repo.extensions()) self._addAction('renameFileMenu', *self._createRenameFileMenu()) self._addAction('remergeFileMenu', *self._createRemergeFileMenu()) @property def repo(self): return self._repoagent.rawRepo() def _runWorkingFileCommand(self, cmdname, fds, opts=None): if not opts: opts = {} paths = [hglib.escapepath(fd.filePath()) for fd in fds] cmdline = hglib.buildcmdargs(cmdname, *paths, **opts) sess = self._runCommand(cmdline) sess.commandFinished.connect(self._notifyChangesOnCommandFinished) return sess @pyqtSlot(int) def _notifyChangesOnCommandFinished(self, ret): if ret == 0: self._notifyChanges() def _notifyChanges(self): # include all selected files for maximum possibility wfiles = [hglib.fromunicode(fd.absoluteFilePath()) for fd in self._selfds] shlib.shell_notify(wfiles) self.refreshNeeded.emit() # this action will no longer be necessary if status widget can toggle # base revision in amend/qrefresh mode @actionSlot(_('Diff &Local'), 'ldiff', 'Ctrl+Shift+D', '', (_indirectbaserev, _notsubroot, _filestatus('MARC!'))) def visualDiffLocalFile(self, fds): self._visualDiff(fds, _lcanonpaths(fds)) @actionSlot(_('&View Missing'), None, None, '', (_isfile, _filestatus('R!'))) def editMissingFile(self, fds): wctx = fds[0].rawContext() self._editFileAt(fds, wctx.p1()) @actionSlot(_('View O&ther'), None, None, '', (_isfile, _merged, _filestatus('MA'))) def editOtherFile(self, fds): wctx = fds[0].rawContext() self._editFileAt(fds, wctx.p2()) @actionSlot(_('&Add'), 'hg-add', None, '', (_notsubroot, _filestatus('RI?'))) def addFile(self, fds): repo = self._repoAgentFor(fds[0]).rawRepo() if 'largefiles' in repo.extensions(): self._addFileWithPrompt(fds) else: self._runWorkingFileCommand('add', fds) def _addFileWithPrompt(self, fds): repo = self._repoAgentFor(fds[0]).rawRepo() result = lfprompt.promptForLfiles(self.parent(), repo.ui, repo, _lcanonpaths(fds)) if not result: return cmdlines = [] for opt, paths in zip(('normal', 'large'), result): if not paths: continue paths = [hglib.escapepath(hglib.tounicode(e)) for e in paths] cmdlines.append(hglib.buildcmdargs('add', *paths, **{opt: True})) sess = self._runCommandSequence(cmdlines) sess.commandFinished.connect(self._notifyChangesOnCommandFinished) @actionSlot(_('Add &Largefiles...'), None, None, '', (_notsubroot, _filestatus('I?'))) def addLargefile(self, fds): self._runWorkingFileCommand('add', fds, {'large': True}) @actionSlot(_('&Forget'), 'hg-remove', None, '', (_notsubroot, _filestatus('MAC!'))) def forgetFile(self, fds): self._runWorkingFileCommand('forget', fds) @actionSlot(_('&Delete Unversioned...'), 'hg-purge', 'Delete', '', (_notsubroot, _filestatus('?I'))) def purgeFile(self, fds): parent = self.parent() files = [hglib.fromunicode(fd.filePath()) for fd in fds] res = qtlib.CustomPrompt( _('Confirm Delete Unversioned'), _('Delete the following unversioned files?'), parent, (_('&Delete'), _('Cancel')), 1, 1, files).run() if res == 1: return opts = {'config': 'extensions.purge=', 'all': True} self._runWorkingFileCommand('purge', fds, opts) @actionSlot(_('Re&move Versioned'), 'hg-remove', None, '', (_notsubroot, _filestatus('C'))) def removeFile(self, fds): self._runWorkingFileCommand('remove', fds) @actionSlot(_('&Revert...'), 'hg-revert', None, '', _filestatus('MAR!')) def revertWorkingFile(self, fds): parent = self.parent() files = _lcanonpaths(fds) wctx = fds[0].rawContext() revertopts = {'date': None, 'rev': '.', 'all': False} if len(wctx.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'] = wctx.p1().rev() elif res == 1: revertopts['rev'] = wctx.p2().rev() else: return elif [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 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 self._runWorkingFileCommand('revert', fds, revertopts) @actionSlot(_('&Copy...'), 'edit-copy', None, '', (_single, _isfile, _filestatus('MC'))) def copyFile(self, fds): self._openRenameDialog(fds, iscopy=True) @actionSlot(_('Re&name...'), 'hg-rename', None, '', (_single, _isfile, _filestatus('MC'))) def renameFile(self, fds): self._openRenameDialog(fds, iscopy=False) def _openRenameDialog(self, fds, iscopy): from tortoisehg.hgqt.rename import RenameDialog srcfd, = fds repoagent = self._repoAgentFor(srcfd) dlg = RenameDialog(repoagent, self.parent(), srcfd.canonicalFilePath(), iscopy=iscopy) if dlg.exec_() == 0: self._notifyChanges() @actionSlot(_('&Ignore...'), 'thg-ignore', None, '', (_notsubroot, _filestatus('?'))) def editHgignore(self, fds): from tortoisehg.hgqt.hgignore import HgignoreDialog repoagent = self._repoAgentFor(fds[0]) parent = self.parent() files = _lcanonpaths(fds) dlg = HgignoreDialog(repoagent, parent, *files) dlg.finished.connect(dlg.deleteLater) dlg.exec_() self._notifyChanges() @actionSlot(_('Edit Re&jects'), None, None, _('Manually resolve rejected patch chunks'), (_single, _isfile, _filestatus('?I'), _filepath(r'\.rej$'))) def editRejects(self, fds): lpath = hglib.fromunicode(fds[0].absoluteFilePath()[:-4]) # drop .rej dlg = rejects.RejectsDialog(self._ui, lpath, self.parent()) if dlg.exec_(): self._notifyChanges() @actionSlot(_('De&tect Renames...'), 'thg-guess', None, '', (_isfile, _filestatus('A?!'))) def guessRename(self, fds): from tortoisehg.hgqt.guess import DetectRenameDialog repoagent = self._repoAgentFor(fds[0]) parent = self.parent() files = _lcanonpaths(fds) dlg = DetectRenameDialog(repoagent, parent, *files) def matched(): ret[0] = True ret = [False] dlg.matchAccepted.connect(matched) dlg.finished.connect(dlg.deleteLater) dlg.exec_() if ret[0]: self._notifyChanges() @actionSlot(_('&Mark Resolved'), None, None, '', (_notsubroot, _mergestatus('U'))) def markFileAsResolved(self, fds): self._runWorkingFileCommand('resolve', fds, {'mark': True}) @actionSlot(_('&Mark Unresolved'), None, None, '', (_notsubroot, _mergestatus('R'))) def markFileAsUnresolved(self, fds): self._runWorkingFileCommand('resolve', fds, {'unmark': True}) @actionSlot(_('Restart Mer&ge'), None, None, '', (_notsubroot, _mergestatus('U'))) def remergeFile(self, fds): self._runWorkingFileCommand('resolve', fds) def _createRenameFileMenu(self): menu = QMenu(_('Was renamed from'), self.parent()) menu.aboutToShow.connect(self._updateRenameFileMenu) menu.triggered.connect(self._renameFrom) fdfilters = (_single, _isfile, _filestatus('?'), _anydeleted) return menu.menuAction(), fdfilters @pyqtSlot() def _updateRenameFileMenu(self): menu = self.sender() assert isinstance(menu, QMenu) menu.clear() fds = self.fileDataListForAction('renameFileMenu') if not fds: return wctx = fds[0].rawContext() for d in wctx.deleted()[:15]: menu.addAction(hglib.tounicode(d)) @pyqtSlot(QAction) def _renameFrom(self, action): fds = self.fileDataListForAction('renameFileMenu') if not fds: # selection might be changed after menu is shown return deleted = hglib.escapepath(action.text()) unknown = hglib.escapepath(fds[0].filePath()) cmdlines = [hglib.buildcmdargs('copy', deleted, unknown, after=True), hglib.buildcmdargs('forget', deleted)] # !->R sess = self._runCommandSequence(cmdlines) sess.commandFinished.connect(self._notifyChangesOnCommandFinished) def _createRemergeFileMenu(self): menu = QMenu(_('Restart Merge &with'), self.parent()) menu.aboutToShow.connect(self._populateRemergeFileMenu) # may be slow menu.triggered.connect(self._remergeFileWith) return menu.menuAction(), (_notsubroot, _mergestatus('U')) @pyqtSlot() def _populateRemergeFileMenu(self): menu = self.sender() assert isinstance(menu, QMenu) menu.aboutToShow.disconnect(self._populateRemergeFileMenu) for tool in hglib.mergetools(self._ui): menu.addAction(hglib.tounicode(tool)) @pyqtSlot(QAction) def _remergeFileWith(self, action): fds = self.fileDataListForAction('remergeFileMenu') self._runWorkingFileCommand('resolve', fds, {'tool': action.text()}) tortoisehg-4.5.2/tortoisehg/hgqt/repomodel.py0000644000175000017500000007665413242076403022240 0ustar sborhosborho00000000000000# 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 __future__ import absolute_import import binascii import os import re from .qtcore import ( QAbstractTableModel, QByteArray, QMimeData, QModelIndex, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QApplication, QColor, QFont, ) from mercurial import ( error, util, ) from ..util import hglib from ..util.i18n import _ from . import ( cmdcore, filedata, graph, graphopt, ) mqpatchmimetype = 'application/thg-mqunappliedpatch' # pick names from "hg help templating" if any GraphColumn = 0 RevColumn = 1 BranchColumn = 2 DescColumn = 3 AuthorColumn = 4 TagsColumn = 5 LatestTagColumn = 6 NodeColumn = 7 AgeColumn = 8 LocalDateColumn = 9 UtcDateColumn = 10 ChangesColumn = 11 ConvertedColumn = 12 PhaseColumn = 13 FileColumn = 14 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')), ('Filename', _('Filename', 'column header')), ) ALLCOLUMNS = tuple(name for name, _text in COLUMNHEADERS) UNAPPLIED_PATCH_COLOR = QColor('#999999') HIDDENREV_COLOR = QColor('#666666') TROUBLED_COLOR = QColor(172, 34, 34) GraphNodeRole = Qt.UserRole + 0 LabelsRole = Qt.UserRole + 1 # [(text, style), ...] 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'(?:(?<=\\\\)|(? self._rowcount: self.beginInsertRows(QModelIndex(), self._rowcount, newlen - 1) self._rowcount = newlen self.endInsertRows() def _shrinkRowCount(self): newlen = len(self.graph) if newlen < self._rowcount: self.beginRemoveRows(QModelIndex(), newlen, self._rowcount - 1) self._rowcount = newlen self.endRemoveRows() def rowCount(self, parent=QModelIndex()): if parent.isValid(): return 0 return self._rowcount def columnCount(self, parent=QModelIndex()): if parent.isValid(): return 0 return len(self.allColumns()) def maxWidthValueForColumn(self, column): if column == RevColumn: return '8' * len(str(len(self.repo))) + '+' if column == NodeColumn: return '8' * 12 + '+' if column in (LocalDateColumn, UtcDateColumn): return hglib.displaytime(util.makedate()) if column in (TagsColumn, LatestTagColumn): try: return sorted(self.repo.tags().keys(), key=lambda x: len(x))[-1][:10] except IndexError: pass if column == BranchColumn: try: return sorted(self.repo.branchmap(), key=lambda x: len(x))[-1] except IndexError: pass if self._hasFileColumn() and column == FileColumn: return self._filename if column == ChangesColumn: return 'Changes' # Fall through for DescColumn return None def rev(self, index): """Revision number of the specified row; None for working-dir""" if not index.isValid(): return -1 gnode = self.graph[index.row()] if gnode.rev is not None and not isinstance(gnode.rev, int): # avoid mixing integer and localstr return -1 return gnode.rev def _user_color(self, user): 'deprecated, please replace with hgtk color scheme' if user not in self._user_colors: idx = graph.hashcolor(user) self._user_colors[user] = graph.COLORS[idx] return self._user_colors[user] def _namedbranch_color(self, branch): 'deprecated, please replace with hgtk color scheme' if branch not in self._branch_colors: idx = graph.hashcolor(branch) self._branch_colors[branch] = graph.COLORS[idx] return self._branch_colors[branch] def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None gnode = self.graph[index.row()] if role == Qt.DisplayRole: if self._hasFileColumn() and index.column() == FileColumn: return hglib.tounicode(gnode.extra[0]) if role == Qt.FontRole: if index.column() in (NodeColumn, ConvertedColumn): return QFont("Monospace") if index.column() == DescColumn and gnode.wdparent: font = QApplication.font('QAbstractItemView') font.setBold(True) return font if role == Qt.ForegroundRole: if (gnode.shape == graph.NODE_SHAPE_UNAPPLIEDPATCH and index.column() != DescColumn): return UNAPPLIED_PATCH_COLOR if role == GraphNodeRole: return gnode # 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 hglib.tounicode(str(e)) else: return None def _safedata(self, index, role): row = index.row() graphlen = len(self.graph) cachelen = len(self._cache) if graphlen > cachelen: self._cache.extend({} for _i in xrange(graphlen - cachelen)) data = self._cache[row] idx = (role, index.column()) if idx not in data: try: result = self._rawdata(index, role) except error.RepoLookupError: # happens if repository pruned/stripped or bundle unapplied # but model is not reloaded yet because repository is busy return None except util.Abort: return None data[idx] = result return data[idx] def _rawdata(self, index, role): row = index.row() column = index.column() gnode = self.graph[row] ctx = self.repo.changectx(gnode.rev) if role == Qt.DisplayRole: textfunc = self._columnmap.get(column) if textfunc is None: return None text = textfunc(self, ctx) if not isinstance(text, unicode): text = hglib.tounicode(text) return text elif role == Qt.ForegroundRole: color = None if gnode.instabilities: color = TROUBLED_COLOR elif column == AuthorColumn and self._authorcolor: color = QColor(self._user_color(ctx.user())) elif column in (GraphColumn, BranchColumn): color = QColor(self._namedbranch_color(ctx.branch())) if index.column() != GraphColumn: if gnode.faded: if color is None: color = HIDDENREV_COLOR else: color = color.lighter() return color elif role == LabelsRole and column == DescColumn: return self._getrevlabels(ctx) elif role == LabelsRole and column == ChangesColumn: return self._getchanges(ctx) return None def flags(self, index): flags = super(HgRepoListModel, self).flags(index) if not index.isValid(): return flags row = index.row() if row >= len(self.graph) and not self.repo.ui.debugflag: # TODO: should not happen; internal data went wrong (issue #754) return Qt.NoItemFlags gnode = self.graph[row] if not self.isActiveRev(gnode.rev): return Qt.NoItemFlags if gnode.shape == graph.NODE_SHAPE_UNAPPLIEDPATCH: flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled if gnode.rev is None: flags |= Qt.ItemIsDropEnabled return flags def isActiveRev(self, rev): """True if the specified rev is not excluded by revset""" return (not self._revspec or rev in self._selectedrevs # consider everything is active while first query is running or (self._revspec and not self._selectedrevs and not self._querysess.isFinished())) def mimeTypes(self): return [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 cmdline = hglib.buildcmdargs('qreorder', after=destpatch, *dragpatches) cmdline = map(hglib.tounicode, cmdline) self._repoagent.runCommand(cmdline) return True def headerData(self, section, orientation, role=Qt.DisplayRole): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self.allColumnHeaders()[section][1] def defaultIndex(self): """Index that should be selected when the model is initially loaded or the row previously selected is gone""" repo = self.repo initialsel = repo.ui.config('tortoisehg', 'initialrevision', 'current') changeid = {'current': '.', 'tip': 'tip', 'workingdir': None, }.get(initialsel, '.') rev = repo[changeid].rev() if self._selectedrevs and rev not in self._selectedrevs: rev = max(self._selectedrevs) index = self.indexFromRev(rev) if index.flags() & Qt.ItemIsEnabled: return index if self._filterbranch: # look for the first active revision as last ditch; should be # removed if filterbranch is merged with revset for row in xrange(len(self.graph)): gnode = self.graph[row] if not isinstance(gnode.rev, int): continue index = self.index(row, 0) if index.flags() & Qt.ItemIsEnabled: return index return QModelIndex() def indexFromRev(self, rev): self._ensureBuilt(rev) self._expandRowCount() try: row = self.graph.index(rev) except ValueError: return QModelIndex() return self.index(row, 0) def _getbranch(self, ctx): 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): 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) and repo.tagtype(t) != 'local'] if tags: self._latesttags[rev] = ctx.date()[0], 0, ':'.join(sorted(tags)) continue try: # The tuples are laid out so the right one can be found by # comparison. if (ctx.parents()): pdate, pdist, ptag = max( self._latesttags[p.rev()] for p in ctx.parents()) else: pdate, pdist, ptag = 0, -1, "" except KeyError: # Cache miss - recurse todo.append(rev) todo.extend(p.rev() for p in ctx.parents()) continue self._latesttags[rev] = pdate, pdist + 1, ptag return self._latesttags[rev][2] def _gettags(self, ctx): 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): 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): try: user = ctx.user() if not self._fullauthorname: user = hglib.username(user) return user except error.Abort: return _('Mercurial User') def _getlog(self, ctx): if ctx.rev() is None: if self.unicodestar: # The Unicode symbol is a black star: return u'\u2605 ' + _('Working Directory') + u' \u2605' else: return '*** ' + _('Working Directory') + ' ***' if self.repo.ui.configbool('tortoisehg', 'longsummary'): limit = 0x7fffffff # unlimited (elide it by view) else: limit = None # first line return hglib.longsummary(ctx.description(), limit) def _getrevlabels(self, ctx): labels = [] # as of hg 4.4.2, repo.branchheads() can be slow because of # branchmap.updatecache() -> scmutil.filteredhash() calls branch = ctx.branch() try: branchheads = self._branchheads[branch] except KeyError: branchheads = set(self.repo.branchheads(branch)) self._branchheads[branch] = branchheads if ctx.rev() is None: for pctx in ctx.parents(): if branchheads and pctx.node() not in branchheads: labels.append((_('Not a head revision!'), 'log.warning')) return labels if ctx.node() in branchheads: labels.append((hglib.tounicode(branch), 'log.branch')) if ctx.thgmqunappliedpatch(): style = 'log.unapplied_patch' labels.append((hglib.tounicode(ctx._patchname), style)) for mark in ctx.bookmarks(): style = 'log.bookmark' if mark == hglib.activebookmark(self.repo): bn = self.repo._bookmarks[hglib.activebookmark(self.repo)] if bn in self.repo.dirstate.parents(): style = 'log.curbookmark' labels.append((hglib.tounicode(mark), style)) for tag in ctx.thgtags(): if self.repo.thgmqtag(tag): style = 'log.patch' else: style = 'log.tag' labels.append((hglib.tounicode(tag), style)) names = set(self.repo.ui.configlist('experimental', 'thg.displaynames')) for name, ns in self.repo.names.iteritems(): if name not in names: continue # we will use the templatename as the color name since those # two should be the same for name in ns.names(self.repo, ctx.node()): labels.append((hglib.tounicode(name), 'log.%s' % ns.colorname)) return labels def _getchanges(self, ctx): """Return the MAR status for the given ctx.""" labels = [] M, A, R = ctx.changesToParent(0) if A: labels.append((str(len(A)), 'log.added')) if M: labels.append((str(len(M)), 'log.modified')) if R: labels.append((str(len(R)), 'log.removed')) return labels def _getconv(self, ctx): 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): if ctx.rev() is None: return '' try: return ctx.phasestr() except: return 'draft' def _hasFileColumn(self): return False # no FileColumn def allColumns(self): if self._hasFileColumn(): return ALLCOLUMNS else: return ALLCOLUMNS[:-1] def allColumnHeaders(self): if self._hasFileColumn(): return COLUMNHEADERS else: return COLUMNHEADERS[:-1] _columnmap = { RevColumn: _getrev, BranchColumn: _getbranch, DescColumn: _getlog, AuthorColumn: _getauthor, TagsColumn: _gettags, LatestTagColumn: _getlatesttags, NodeColumn: lambda self, ctx: str(ctx), AgeColumn: lambda self, ctx: hglib.age(ctx.date()).decode('utf-8'), LocalDateColumn: lambda self, ctx: hglib.displaytime(ctx.date()), UtcDateColumn: lambda self, ctx: hglib.utctime(ctx.date()), ConvertedColumn: _getconv, PhaseColumn: _getphase, } class FileRevModel(HgRepoListModel): """ Model used to manage the list of revisions of a file, in file viewer of in diff-file viewer dialogs. """ _defaultcolumns = ('Graph', 'Rev', 'Branch', 'Description', 'Author', 'Age', 'Filename') def __init__(self, repoagent, filename, parent=None): self._filename = filename HgRepoListModel.__init__(self, repoagent, parent) def _hasFileColumn(self): return True def _createGraph(self): grapher = graph.filelog_grapher(self.repo, self._filename) return graph.Graph(self.repo, grapher) 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 QModelIndex() return self.indexFromRev(fctx.linkrev()) def fileData(self, index, baseindex=QModelIndex()): """Displayable file data at the given index; baseindex specifies the revision where status is calculated from""" row = index.row() if not index.isValid() or row < 0 or row >= len(self.graph): return filedata.createNullData(self.repo) rev = self.graph[row].rev ctx = self.repo.changectx(rev) if baseindex.isValid(): prev = self.graph[baseindex.row()].rev pctx = self.repo.changectx(prev) else: pctx = ctx.p1() filename = self.graph.filename(rev) if filename in pctx: status = 'M' else: status = 'A' return filedata.createFileData(ctx, pctx, filename, status) tortoisehg-4.5.2/tortoisehg/hgqt/shelve.py0000644000175000017500000005207113153775104021530 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import os import time from .qtcore import ( QSettings, Qt, pyqtSlot, ) from .qtgui import ( QAction, QComboBox, QDialog, QFrame, QHBoxLayout, QKeySequence, QPushButton, QSplitter, QToolBar, QVBoxLayout, ) from mercurial import ( commands, error, util, ) from ..util import hglib from ..util.i18n import _ from ..util.patchctx import patchctx from . import ( cmdui, chunks, qtlib, ) class ShelveDialog(QDialog): wdir = _('Working Directory') def __init__(self, repoagent, parent=None): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.setWindowIcon(qtlib.geticon('hg-shelve')) self._repoagent = repoagent self.shelves = [] self.patches = [] self._patchnames = {} # path: mq patch name 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.setContentsMargins(2, 2, 2, 2) aframe.setLayout(avbox) ahbox = QHBoxLayout() ahbox.setSpacing(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) 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.setContentsMargins(2, 2, 2, 2) bframe.setLayout(bvbox) bhbox = QHBoxLayout() bhbox.setSpacing(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) 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.setIconSize(qtlib.toolBarIconSize()) self.lefttbar.setStyleSheet(qtlib.tbstylesheet) self.tbarhbox.addWidget(self.lefttbar) self.deletea = a = QAction(_('Delete selected chunks'), self) self.deletea.triggered.connect(self.deleteChunksA) 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.setIconSize(qtlib.toolBarIconSize()) 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.setIconSize(qtlib.toolBarIconSize()) 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.deleteChunksB) 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') % repoagent.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 deleteChunksA(self): if self.comboa.currentIndex() == 0: msg = _('Delete selected chunks from working copy?') else: f = hglib.tounicode(os.path.basename(self.currentPatchA())) msg = _('Delete selected chunks from shelf file %s?') % f if qtlib.QuestionMsgBox(_('Are you sure?'), msg, parent=self): self.browsea.deleteSelectedChunks() @pyqtSlot() def deleteChunksB(self): f = hglib.tounicode(os.path.basename(self.currentPatchB())) msg = _('Delete selected chunks from shelf file %s?') % f if qtlib.QuestionMsgBox(_('Are you sure?'), msg, parent=self): 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.vfs.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._patchnames = dict(zip(self.patches, 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() rev = self._patchnames.get(rev, rev) 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() rev = self._patchnames.get(rev, rev) 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(str) def linkActivated(self, linktext): pass @pyqtSlot(str) 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(qtlib.readByteArray(s, wb + 'geometry')) self.splitter.restoreState(qtlib.readByteArray(s, wb + 'panesplitter')) self.browsea.splitter.restoreState( qtlib.readByteArray(s, wb + 'filesplitter')) self.browseb.splitter.restoreState( qtlib.readByteArray(s, wb + 'filesplitter')) self.browsea.loadSettings(s, wb + 'fileviewa') self.browseb.loadSettings(s, wb + 'fileviewb') def reject(self): self.storeSettings() super(ShelveDialog, self).reject() tortoisehg-4.5.2/tortoisehg/hgqt/repowidget.py0000644000175000017500000026003013155510714022403 0ustar sborhosborho00000000000000# 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. from __future__ import absolute_import import binascii import cStringIO import os import shlex # used by runCustomCommand import subprocess # used by runCustomCommand from .qtcore import ( QFile, QIODevice, QItemSelectionModel, QMimeData, QSettings, QTimer, QUrl, Qt, pyqtSignal, pyqtSlot, ) from .qtgui import ( QAction, QApplication, QDesktopServices, QFileDialog, QIcon, QKeySequence, QMainWindow, QMenu, QMessageBox, QSplitter, QTabWidget, QVBoxLayout, QWidget, ) from mercurial import ( error, patch, phases, util, ) from ..util import ( hglib, paths, shlib, ) from ..util.i18n import _ from . import ( archive, backout, bisect, bookmark, cmdcore, cmdui, compress, graft, hgemail, infobar, matching, merge, mq, postreview, prune, purge, qtlib, rebase, repomodel, resolve, revdetails, settings, shelve, sign, tag, thgimport, thgstrip, update, visdiff, ) from .commit import CommitWidget from .docklog import ConsoleWidget from .grep import SearchWidget from .pbranch import PatchBranchWidget from .qtlib import ( DemandWidget, InfoMsgBox, QuestionMsgBox, WarningMsgBox, ) from .repofilter import RepoFilterBar from .repoview import HgRepoView from .sync import SyncWidget # 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 _ENABLE_MENU_FUNCS = { '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), 'istrue' : lambda ap, wd, tags: True, } class RepoWidget(QWidget): currentTaskTabChanged = pyqtSignal() showMessageSignal = pyqtSignal(str) taskTabVisibilityChanged = pyqtSignal(bool) toolbarVisibilityChanged = pyqtSignal(bool) # TODO: progress can be removed if all actions are run as hg command progress = pyqtSignal(str, object, str, str, object) makeLogVisible = pyqtSignal(bool) revisionSelected = pyqtSignal(object) titleChanged = pyqtSignal(str) """Emitted when changed the expected title for the RepoWidget tab""" busyIconChanged = pyqtSignal() repoLinkClicked = pyqtSignal(str) """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 self.bundlesource = None # source URL of incoming bundle [unicode] self.outgoingMode = False self._busyIconNames = [] self._namedTabs = {} self.destroyed.connect(self.repo.thginvalidate) self.currentMessage = '' self.setupUi() self.createActions() self.loadSettings() self._initModel() self._lastTaskTabVisible = self.isTaskTabVisible() self.repotabs_splitter.splitterMoved.connect(self._onSplitterMoved) if bundle: self.setBundle(bundle) self._dialogs = qtlib.DialogKeeper( lambda self, dlgmeth, *args: dlgmeth(self, *args), parent=self) # listen to change notification after initial settings are loaded repoagent.repositoryChanged.connect(self.repositoryChanged) repoagent.configChanged.connect(self.configChanged) QTimer.singleShot(0, self._initView) def setupUi(self): self.repotabs_splitter = QSplitter(orientation=Qt.Vertical) self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setSpacing(0) # placeholder to shift repoview while infobar is overlaid self._repoviewFrame = infobar.InfoBarPlaceholder(self._repoagent, self) self._repoviewFrame.linkActivated.connect(self._openLink) self.filterbar = RepoFilterBar(self._repoagent, 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.setRevisionSet.connect(self.setRevisionSet) self.filterbar.filterToggled.connect(self.filterToggled) self.filterbar.visibilityChanged.connect(self.toolbarVisibilityChanged) self.filterbar.hide() self.layout().addWidget(self.repotabs_splitter) cs = ('Workbench', _('Workbench Log Columns')) self.repoview = view = HgRepoView(self._repoagent, 'repoWidget', cs, self) view.clicked.connect(self._clearInfoMessage) view.revisionSelected.connect(self.onRevisionSelected) view.revisionActivated.connect(self.onRevisionActivated) view.showMessage.connect(self.showMessage) view.menuRequested.connect(self.viewMenuRequest) self._repoviewFrame.setView(view) self.repotabs_splitter.addWidget(self._repoviewFrame) 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() tt.currentChanged.connect(self.currentTaskTabChanged) w = revdetails.RevDetailsWidget(self._repoagent, self) self.revDetailsWidget = w 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.revsetFilterRequested.connect(self.setFilter) w.runCustomCommandRequested.connect( self.handleRunCustomCommandRequest) 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) idx = tt.addTab(w, qtlib.geticon('hg-commit'), '') self._namedTabs['commit'] = idx tt.setTabToolTip(idx, _("Commit", "tab tooltip")) self.grepDemand = w = DemandWidget('createGrepWidget', self) idx = tt.addTab(w, qtlib.geticon('hg-grep'), '') self._namedTabs['grep'] = idx tt.setTabToolTip(idx, _("Search", "tab tooltip")) w = ConsoleWidget(self._repoagent, self) self.consoleWidget = w w.closeRequested.connect(self.switchToPreferredTaskTab) idx = tt.addTab(w, qtlib.geticon('thg-console'), '') self._namedTabs['console'] = idx tt.setTabToolTip(idx, _("Console log", "tab tooltip")) self.syncDemand = w = DemandWidget('createSyncWidget', self) idx = tt.addTab(w, qtlib.geticon('thg-sync'), '') self._namedTabs['sync'] = idx tt.setTabToolTip(idx, _("Synchronize", "tab tooltip")) if 'pbranch' in self.repo.extensions(): self.pbranchDemand = w = DemandWidget('createPatchBranchWidget', self) idx = tt.addTab(w, qtlib.geticon('hg-branch'), '') tt.setTabToolTip(idx, _("Patch Branch", "tab tooltip")) self._namedTabs['pbranch'] = idx @pyqtSlot() def _initView(self): self._updateRepoViewForModel() # restore column widths when model is initially loaded. For some # reason, this needs to be deferred after updating the view. Otherwise # repoview.HgRepoView.resizeEvent() fires as the vertical scrollbar is # added, which causes the last column to grow by the scrollbar width on # each restart (and steal from the description width). QTimer.singleShot(0, self.repoview.resizeColumns) # select the widget chosen by the user name = self.repo.ui.config('tortoisehg', 'defaultwidget') if name: name = {'revdetails': 'log', 'search': 'grep'}.get(name, name) self.taskTabsWidget.setCurrentIndex(self._namedTabs.get(name, 0)) def currentTaskTabName(self): indexmap = dict((idx, name) for name, idx in self._namedTabs.iteritems()) return indexmap.get(self.taskTabsWidget.currentIndex()) @pyqtSlot(str) 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 self.setTaskTabVisible(True) def isTaskTabVisible(self): return self.repotabs_splitter.sizes()[1] > 0 def setTaskTabVisible(self, visible): if visible == self.isTaskTabVisible(): return if visible: self.repotabs_splitter.setSizes([1, 1]) else: self.repotabs_splitter.setSizes([1, 0]) self._updateLastTaskTabState(visible) @pyqtSlot() def _onSplitterMoved(self): visible = self.isTaskTabVisible() if self._lastTaskTabVisible == visible: return self._updateLastTaskTabState(visible) def _updateLastTaskTabState(self, visible): self._lastTaskTabVisible = visible self.taskTabVisibilityChanged.emit(visible) @property def repo(self): return self._repoagent.rawRepo() def repoRootPath(self): return self._repoagent.rootPath() def repoDisplayName(self): return self._repoagent.displayName() def title(self): """Returns the expected title for this widget [unicode]""" name = self._repoagent.shortName() if self._repoagent.overlayUrl(): return _('%s ') % name elif self.repomodel.branch(): return u'%s [%s]' % (name, self.repomodel.branch()) else: return name def busyIcon(self): if self._busyIconNames: return qtlib.geticon(self._busyIconNames[-1]) else: return QIcon() def filterBar(self): return self.filterbar def filterBarVisible(self): return self.filterbar.isVisible() @pyqtSlot(bool) def toggleFilterBar(self, checked): """Toggle display repowidget filter bar""" if self.filterbar.isVisibleTo(self) == checked: return self.filterbar.setVisible(checked) if checked: self.filterbar.setFocus() 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(str) 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): return self._repoviewFrame.setInfoBar(cls, *args, **kwargs) def clearInfoBar(self, priority=None): return self._repoviewFrame.clearInfoBar(priority) 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 createSyncWidget(self): sw = SyncWidget(self._repoagent, self) sw.newCommand.connect(self._handleNewSyncCommand) sw.outgoingNodes.connect(self.setOutgoingNodes) sw.showMessage.connect(self.showMessage) sw.showMessage.connect(self._repoviewFrame.showMessage) sw.incomingBundle.connect(self.setBundle) sw.pullCompleted.connect(self.onPullCompleted) sw.pushCompleted.connect(self.clearRevisionSet) sw.refreshTargets(self.rev) sw.switchToRequest.connect(self.switchToNamedTaskTab) return sw @pyqtSlot(cmdcore.CmdSession) def _handleNewSyncCommand(self, sess): self._handleNewCommand(sess) if sess.isFinished(): return sess.commandFinished.connect(self._onSyncCommandFinished) self._setBusyIcon('thg-sync') @pyqtSlot() def _onSyncCommandFinished(self): self._clearBusyIcon('thg-sync') def _setBusyIcon(self, iconname): self._busyIconNames.append(iconname) self.busyIconChanged.emit() def _clearBusyIcon(self, iconname): if iconname in self._busyIconNames: self._busyIconNames.remove(iconname) self.busyIconChanged.emit() @pyqtSlot(str) def setFilter(self, filter): self.filterbar.setQuery(filter) self.filterbar.setVisible(True) self.filterbar.runQuery() @pyqtSlot(str, str) def setBundle(self, bfile, bsource=None): if self._repoagent.overlayUrl(): self.clearBundle() self.bundlesource = bsource and unicode(bsource) or None oldlen = len(self.repo) # no "bundle:" because bfile may contain "+" separator self._repoagent.setOverlay(bfile) self.filterbar.setQuery('bundle()') self.filterbar.runQuery() self.titleChanged.emit(self.title()) newlen = len(self.repo) w = self.setInfoBar(infobar.ConfirmInfoBar, _('Found %d incoming changesets') % (newlen - oldlen)) assert w w.acceptButton.setText(_('Pull')) w.acceptButton.setToolTip(_('Pull incoming changesets into ' 'your repository')) w.rejectButton.setText(_('Cancel')) w.rejectButton.setToolTip(_('Reject incoming changesets')) w.accepted.connect(self.acceptBundle) w.rejected.connect(self.clearBundle) @pyqtSlot() def clearBundle(self): self.clearRevisionSet() self.bundlesource = None self._repoagent.clearOverlay() self.titleChanged.emit(self.title()) @pyqtSlot() def onPullCompleted(self): if self._repoagent.overlayUrl(): self.clearBundle() @pyqtSlot() def acceptBundle(self): bundle = self._repoagent.overlayUrl() if bundle: w = self.syncDemand.get() w.pullBundle(bundle, None, self.bundlesource) @pyqtSlot() def pullBundleToRev(self): bundle = self._repoagent.overlayUrl() if bundle: # manually remove infobar to work around unwanted clearBundle # during pull operation (issue #2596) self._repoviewFrame.discardInfoBar() w = self.syncDemand.get() w.pullBundle(bundle, self.repo[self.rev].hex(), self.bundlesource) @pyqtSlot() def clearRevisionSet(self): self.filterbar.setQuery('') self.setRevisionSet('') def setRevisionSet(self, revspec): self.repomodel.setRevset(revspec) if not revspec: self.outgoingMode = False @pyqtSlot(bool) def filterToggled(self, checked): self.repomodel.setFilterByRevset(checked) def setOutgoingNodes(self, nodes): self.filterbar.setQuery('outgoing()') revs = [self.repo[n].rev() for n in nodes] self.setRevisionSet(hglib.compactrevs(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(infobar.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 gotoRev(self, revspec): """Select and scroll to the specified revision""" try: # try instant look up if hglib.fromunicode(revspec) in self.repo: self.repoview.goto(revspec) return except error.LookupError: pass # ambiguous node cmdline = hglib.buildcmdargs('log', rev=revspec, template='{rev}\n') sess = self._runCommand(cmdline) sess.setCaptureOutput(True) sess.commandFinished.connect(self._onGotoRevQueryFinished) @pyqtSlot(int) def _onGotoRevQueryFinished(self, ret): sess = self.sender() if ret != 0: # warnings (e.g. "ambiguous identifier") aren't captured by # showOutput(), so we have to set the error explicitly. self.setInfoBar(infobar.CommandErrorInfoBar, sess.errorString() or sess.warningString()) return output = str(sess.readAll()) if not output: # TODO: maybe this should be a warning bar since there would be no # information in log window. self.setInfoBar(infobar.CommandErrorInfoBar, _('No revision found')) return rev = int(output.splitlines()[-1]) # pick last rev as "hg update" does self.repoview.goto(rev) def showMessage(self, msg): self.currentMessage = msg if self.isVisible(): self.showMessageSignal.emit(msg) def keyPressEvent(self, event): if self._repoviewFrame.activeInfoBar() and event.key() == Qt.Key_Escape: self.clearInfoBar(infobar.INFO) else: QWidget.keyPressEvent(self, event) def showEvent(self, event): QWidget.showEvent(self, event) self.showMessageSignal.emit(self.currentMessage) if not event.spontaneous(): # RepoWidget must be the main widget in any window, so grab focus # when it gets visible at start-up or by switching tabs. self.repoview.setFocus() 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) data = patch.extract(self.repo.ui, pf) filename = data.get('filename') if filename: filepaths.append(p) os.unlink(filename) except EnvironmentError: 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): self._dialogs.open(RepoWidget._createBisectDialog) def _createBisectDialog(self): dlg = bisect.BisectDialog(self._repoagent, self) dlg.newCandidate.connect(self.gotoParent) return dlg def resolve(self): dlg = resolve.ResolveDialog(self._repoagent, self) dlg.exec_() def thgimport(self, paths=None): dlg = thgimport.ImportDialog(self._repoagent, self) if paths: dlg.setfilepaths(paths) if dlg.exec_() == 0: self.gotoTip() def unbundle(self): w = self.syncDemand.get() w.unbundle() def shelve(self, arg=None): self._dialogs.open(RepoWidget._createShelveDialog) def _createShelveDialog(self): dlg = shelve.ShelveDialog(self._repoagent) 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.repoDisplayName()) 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.repoDisplayName()) dlg.setWindowFlags(dlg.windowFlags() | Qt.WindowMaximizeButtonHint) dlg.setSession(self._repoagent.runCommand(cmdline, self)) dlg.exec_() def rollback(self): desc, oldlen = hglib.readundodesc(self.repo) if not desc: InfoMsgBox(_('No transaction available'), _('There is no rollback transaction available')) return elif desc == 'commit': if not QuestionMsgBox(_('Undo last commit?'), _('Undo most recent commit (%d), preserving file changes?') % oldlen): return else: if not QuestionMsgBox(_('Undo last transaction?'), _('Rollback to revision %d (undo %s)?') % (oldlen - 1, desc)): return try: rev = self.repo['.'].rev() except error.LookupError, e: InfoMsgBox(_('Repository Error'), _('Unable to determine working copy revision\n') + hglib.tounicode(e)) return if rev >= oldlen 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_() # ignores result code of PurgeDialog because it's unreliable self._refreshCommitTabIfNeeded() ## End workbench event forwards @pyqtSlot(str, 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._namedTabs['grep']) self.grepDemand.setSearch(pattern, **opts) self.grepDemand.runSearch() def _initModel(self): self.repomodel = repomodel.HgRepoListModel(self._repoagent, self) self.repomodel.setBranch(self.filterbar.branch(), self.filterbar.branchAncestorsIncluded()) self.repomodel.setFilterByRevset(self.filterbar.filtercb.isChecked()) self.repomodel.setShowGraftSource(self.filterbar.getShowGraftSource()) self.repomodel.showMessage.connect(self.showMessage) self.repomodel.showMessage.connect(self._repoviewFrame.showMessage) self.repoview.setModel(self.repomodel) self.repomodel.revsUpdated.connect(self._updateRepoViewForModel) @pyqtSlot() def _updateRepoViewForModel(self): model = self.repoview.model() selmodel = self.repoview.selectionModel() index = selmodel.currentIndex() if not (index.flags() & Qt.ItemIsEnabled): index = model.defaultIndex() f = QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows selmodel.setCurrentIndex(index, f) self.repoview.scrollTo(index) self.repoview.enablefilterpalette(bool(model.revset())) self.clearInfoBar(infobar.INFO) # clear progress message @pyqtSlot() def _clearInfoMessage(self): self.clearInfoBar(infobar.INFO) @pyqtSlot() def switchToPreferredTaskTab(self): tw = self.taskTabsWidget rev = self.rev 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._namedTabs['commit']) else: # Clicking on a normal revision switches from commit tab tw.setCurrentIndex(self._namedTabs['log']) def onRevisionSelected(self, rev): 'View selection changed, could be a reload' self.showMessage('') try: self.revDetailsWidget.onRevisionSelected(rev) self.revisionSelected.emit(rev) if type(rev) != str: # Regular patch or working directory self.grepDemand.forward('setRevision', rev) self.syncDemand.forward('refreshTargets', rev) self.commitDemand.forward('setRev', rev) except (IndexError, error.RevlogError, error.Abort), e: self.showMessage(hglib.tounicode(str(e))) cw = self.taskTabsWidget.currentWidget() if cw.canswitch(): self.switchToPreferredTaskTab() @pyqtSlot() def gotoParent(self): self.goto('.') def gotoTip(self): self.repoview.clearSelection() self.goto('tip') def goto(self, 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('') self.filterbar.refresh() self.repoview.saveSettings() def reloadTaskTab(self): w = self.taskTabsWidget.currentWidget() w.reload() @pyqtSlot() def repositoryChanged(self): 'Repository has detected a changelog / dirstate change' try: self.rebuildGraph() except (error.RevlogError, error.RepoError), e: self.showMessage(hglib.tounicode(str(e))) @pyqtSlot() def configChanged(self): 'Repository is reporting its config files have changed' 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(str, bool) def setBranch(self, branch, allparents): self.repomodel.setBranch(branch, allparents=allparents) self.titleChanged.emit(self.title()) @pyqtSlot(bool) def setShowHidden(self, showhidden): self._repoagent.setHiddenRevsIncluded(showhidden) @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 = hglib.shortrepoid(self.repo) self.revDetailsWidget.loadSettings(s) self.filterbar.loadSettings(s) self._repoagent.setHiddenRevsIncluded(self.filterbar.getShowHidden()) self.repotabs_splitter.restoreState( qtlib.readByteArray(s, 'repoWidget/splitter-' + repoid)) def okToContinue(self): if self._repoagent.isBusy(): r = 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 r == QMessageBox.Yes: self._repoagent.abortCommands() return False for i in xrange(self.taskTabsWidget.count()): w = self.taskTabsWidget.widget(i) if w.canExit(): continue self.taskTabsWidget.setCurrentWidget(w) self.showMessage(_('Tab cannot exit')) 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 = hglib.shortrepoid(self.repo) s.setValue('repoWidget/splitter-' + repoid, self.repotabs_splitter.saveState()) except EnvironmentError: pass self.revDetailsWidget.saveSettings(s) self.commitDemand.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 def syncBookmark(self): self.syncDemand.get().syncBookmark() ## ## 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._repoagent.overlayUrl(): 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 _selectionMenuExec(self, point, selection, menu, menuitems): ctxs = [self.repo.changectx(rev) for rev in selection] applied = [ctx.thgmqappliedpatch() for ctx in ctxs] working = [ctx.rev() is None for ctx in ctxs] tags = [ctx.tags() for ctx in ctxs] for item in menuitems: enabled = all(item.enableFunc(*args) for args in zip(applied, working, tags)) item.setEnabled(enabled) menu.exec_(point) def singleSelectionMenu(self, point, selection): self._selectionMenuExec(point, selection, self.singlecmenu, self.singlecmenuitems) 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._selectionMenuExec(point, selection, self.paircmenu, self.paircmenuitems) 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._selectionMenuExec(point, selection, self.multicmenu, self.multicmenuitems) 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(len(selection) == 1) self.unappcmenu.exec_(point) def _createMenuEntry(self, items, menu, ext=None, func=None, desc=None, icon=None, cb=None): if ext and ext not in self.repo.extensions(): 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 def _setupCustomSubmenu(self, items, menu, location): tools, toollist = hglib.tortoisehgtools(self.repo.ui, selectedlocation=location) if not tools: return self._createMenuEntry(items, menu) submenu = menu.addMenu(_('Custom Tools')) submenu.triggered.connect(self._runCustomCommandByMenu) for name in toollist: if name == '|': self._createMenuEntry(items, 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 _ENABLE_MENU_FUNCS: enable = _ENABLE_MENU_FUNCS[enable] else: continue a = self._createMenuEntry(items, submenu, None, enable, label, icon) a.setData((command, showoutput, workingdir)) def generateSingleMenu(self, mode=None): items = [] # This menu will never be opened for an unapplied patch, they # have their own menu. entry = self._createMenuEntry enablefuncs = _ENABLE_MENU_FUNCS 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(items, submenu, None, enablefuncs['isrev'], _('Push to &Here'), pushtypeicon['revision'], self.pushToRevision) entry(items, submenu, None, enablefuncs['isrev'], _('Push Selected &Branch'), pushtypeicon['branch'], self.pushBranch) entry(items, submenu, None, enablefuncs['isrev'], _('Push &All'), pushtypeicon['all'], self.pushAll) entry(items, menu) entry(items, menu, None, enablefuncs['isrev'], _('&Update...'), 'hg-update', self.updateToRevision) entry(items, menu) entry(items, menu, None, enablefuncs['isctx'], _('&Diff to Parent'), 'visualdiff', self.visualDiffRevision) entry(items, menu, None, enablefuncs['isrev'], _('Diff to &Local'), 'ldiff', self.visualDiffToLocal) entry(items, menu, None, enablefuncs['isctx'], _('Bro&wse at Revision'), 'hg-annotate', self.manifestRevision) act = self._createFilterBySelectedRevisionsMenu() act.enableFunc = enablefuncs['isrev'] menu.addAction(act) items.append(act) entry(items, menu) entry(items, menu, None, enablefuncs['fixed'], _('&Merge with Local...'), 'hg-merge', self.mergeWithRevision) entry(items, menu) entry(items, menu, None, enablefuncs['fixed'], _('&Tag...'), 'hg-tag', self.tagToRevision) entry(items, menu, None, enablefuncs['isrev'], _('Boo&kmark...'), 'hg-bookmarks', self.bookmarkRevision) entry(items, menu, 'gpg', enablefuncs['fixed'], _('Sig&n...'), 'hg-sign', self.signRevision) entry(items, menu) entry(items, menu, None, enablefuncs['fixed'], _('&Backout...'), 'hg-revert', self.backoutToRevision) entry(items, menu, None, enablefuncs['isctx'], _('Revert &All Files...'), 'hg-revert', self.revertToRevision) entry(items, menu) entry(items, menu, None, enablefuncs['isrev'], _('Copy &Hash'), 'copy-hash', self.copyHash) entry(items, menu) submenu = menu.addMenu(_('E&xport')) entry(items, submenu, None, enablefuncs['isrev'], _('E&xport Patch...'), 'hg-export', self.exportRevisions) entry(items, submenu, None, enablefuncs['isrev'], _('&Email Patch...'), 'mail-forward', self.emailSelectedRevisions) entry(items, submenu, None, enablefuncs['isrev'], _('&Archive...'), 'hg-archive', self.archiveRevision) entry(items, submenu, None, enablefuncs['isrev'], _('&Bundle Rev and Descendants...'), 'hg-bundle', self.bundleRevisions) entry(items, submenu, None, enablefuncs['isctx'], _('&Copy Patch'), 'copy-patch', self.copyPatch) entry(items, menu) submenu = menu.addMenu(_('Change &Phase to')) submenu.triggered.connect(self._changePhaseByMenu) for pnum, pname in enumerate(phases.phasenames): a = entry(items, submenu, None, enablefuncs['isrev'], pname) a.setData(pnum) entry(items, menu) entry(items, menu, None, enablefuncs['isrev'], _('&Graft to Local...'), 'hg-transplant', self.graftRevisions) exs = self.repo.extensions() if 'mq' in exs or 'rebase' in exs or 'strip' in exs or 'evolve' in exs: submenu = menu.addMenu(_('Modi&fy History')) entry(items, submenu, 'mq', enablefuncs['applied'], _('&Unapply Patch'), 'hg-qgoto', self.qgotoParentRevision) entry(items, submenu, 'mq', enablefuncs['fixed'], _('Import to &MQ'), 'qimport', self.qimportRevision) entry(items, submenu, 'mq', enablefuncs['applied'], _('&Finish Patch'), 'qfinish', self.qfinishRevision) entry(items, submenu, 'mq', enablefuncs['applied'], _('Re&name Patch...'), None, self.qrename) entry(items, submenu, 'mq') if self._mqActions: entry(items, submenu, 'mq', enablefuncs['isctx'], _('MQ &Options'), None, self._mqActions.launchOptionsDialog) entry(items, submenu, 'mq') entry(items, submenu, 'rebase', enablefuncs['isrev'], _('&Rebase...'), 'hg-rebase', self.rebaseRevision) entry(items, submenu, 'rebase') entry(items, submenu, 'evolve', enablefuncs['fixed'], _('&Prune...'), 'edit-cut', self._pruneSelected) if 'mq' in exs or 'strip' in exs: entry(items, submenu, None, enablefuncs['fixed'], _('&Strip...'), 'hg-strip', self.stripRevision) entry(items, menu, 'reviewboard', enablefuncs['isrev'], _('Post to Re&view Board...'), 'reviewboard', self.sendToReviewBoard) entry(items, menu, 'rupdate', enablefuncs['fixed'], _('&Remote Update...'), 'hg-update', self.rupdate) self._setupCustomSubmenu(items, menu, 'workbench.revdetails.custom-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 # simply disable lazy evaluation as we won't handle slow query return list(self.repo.revs('%s::%s' % (A, B))) 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, _filter = QFileDialog.getSaveFileName( self, _('Write diff file'), hglib.tounicode(os.path.join(root, filename))) if not file: return f = QFile(file) if not f.open(QIODevice.WriteOnly | QIODevice.Truncate): WarningMsgBox(_('Repository Error'), _('Unable to write diff file')) return sess = self._buildPatch('diff') sess.setOutputDevice(f) def archiveDagRange(): l = dagrange() if l: self.archiveRevisions(l) 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 dlg = self._dialogs.open(RepoWidget._createBisectDialog) dlg.restart(str(revA), str(revB)) def bisectReverse(): revA, revB = self.menuselection dlg = self._dialogs.open(RepoWidget._createBisectDialog) dlg.restart(str(revB), str(revA)) 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_() items = [] entry = self._createMenuEntry enablefuncs = _ENABLE_MENU_FUNCS menu = QMenu(self) entry(items, menu, None, enablefuncs['istrue'], _('Visual Diff...'), 'visualdiff', diffPair) entry(items, menu, None, enablefuncs['istrue'], _('Export Diff...'), 'hg-export', exportDiff) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Export Selected...'), 'hg-export', exportPair) entry(items, menu, None, enablefuncs['istrue'], _('Email Selected...'), 'mail-forward', emailPair) entry(items, menu, None, enablefuncs['istrue'], _('Copy Selected as Patch'), 'copy-patch', self.copyPatch) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Archive DAG Range...'), 'hg-archive', archiveDagRange) entry(items, menu, None, enablefuncs['istrue'], _('Export DAG Range...'), 'hg-export', exportDagRange) entry(items, menu, None, enablefuncs['istrue'], _('Email DAG Range...'), 'mail-forward', emailDagRange) entry(items, menu, None, enablefuncs['istrue'], _('Bundle DAG Range...'), 'hg-bundle', bundleDagRange) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Bisect - Good, Bad...'), 'hg-bisect-good-bad', bisectNormal) entry(items, menu, None, enablefuncs['istrue'], _('Bisect - Bad, Good...'), 'hg-bisect-bad-good', bisectReverse) entry(items, menu, None, enablefuncs['istrue'], _('Compress History...'), 'hg-compress', compressDlg) entry(items, menu, 'rebase', enablefuncs['istrue'], _('Rebase...'), 'hg-rebase', rebaseDlg) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Goto common ancestor'), 'hg-merge', self._gotoAncestor) menu.addAction(self._createFilterBySelectedRevisionsMenu()) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Graft Selected to local...'), 'hg-transplant', self.graftRevisions) entry(items, menu) entry(items, menu, 'evolve', enablefuncs['istrue'], _('&Prune Selected...'), 'edit-cut', self._pruneSelected) 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._setupCustomSubmenu(items, menu, 'workbench.pairselection.custom-menu') self.paircmenu = menu self.paircmenuitems = items 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) entry = self._createMenuEntry enablefuncs = _ENABLE_MENU_FUNCS items = [] menu = QMenu(self) entry(items, menu, None, enablefuncs['istrue'], _('Export Selected...'), 'hg-export', exportSel) entry(items, menu, None, enablefuncs['istrue'], _('Email Selected...'), 'mail-forward', emailSel) entry(items, menu, None, enablefuncs['istrue'], _('Copy Selected as Patch'), 'copy-patch', self.copyPatch) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Goto common ancestor'), 'hg-merge', self._gotoAncestor) menu.addAction(self._createFilterBySelectedRevisionsMenu()) entry(items, menu) entry(items, menu, None, enablefuncs['istrue'], _('Graft Selected to local...'), 'hg-transplant', self.graftRevisions) if 'evolve' in self.repo.extensions(): menu.addSeparator() entry(items, menu, None, enablefuncs['istrue'], _('&Prune Selected...'), 'edit-cut', self._pruneSelected) entry(items, menu, 'reviewboard', enablefuncs['istrue'], _('Post Selected to Review Board...'), None, self.sendToReviewBoard) self._setupCustomSubmenu(items, menu, 'workbench.multipleselection.custom-menu') self.multicmenu = menu self.multicmenuitems = items 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, _filter = 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) ename = self._repoagent.shortName() + '_%r.patch' epath = os.path.join(udir, ename) custompath = False cmdline = hglib.buildcmdargs('export', verbose=True, output=epath, rev=hglib.compactrevs(sorted(revisions))) 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 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 = None if isinstance(self.rev, int): rev = hglib.getrevisionlabel(self.repo, self.rev) dlg = update.UpdateDialog(self._repoagent, rev, self) r = dlg.exec_() if r in (0, 1): self.gotoParent() @pyqtSlot() def lockTool(self): from .locktool import LockDialog dlg = LockDialog(self._repoagent, self) if dlg: dlg.exec_() @pyqtSlot() def revertToRevision(self): 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=self.rev) sess = self._runCommand(cmdline) sess.commandFinished.connect(self._refreshCommitTabIfNeeded) def _createFilterBySelectedRevisionsMenu(self): menu = QMenu(_('Filter b&y'), self) menu.setIcon(qtlib.geticon('view-filter')) menu.triggered.connect(self._filterBySelectedRevisions) for t, r in [(_('&Ancestors and Descendants'), "ancestors({revs}) or descendants({revs})"), (_('A&uthor'), "matching({revs}, 'author')"), (_('&Branch'), "branch({revs})"), ]: a = menu.addAction(t) a.setData(r) menu.addSeparator() menu.addAction(_('&More Options...')) return menu.menuAction() @pyqtSlot(QAction) def _filterBySelectedRevisions(self, action): revs = hglib.compactrevs(sorted(self.repoview.selectedRevisions())) expr = str(action.data()) if not expr: self._filterByMatchDialog(revs) return self.setFilter(expr.format(revs=revs)) def _filterByMatchDialog(self, revlist): dlg = matching.MatchDialog(self._repoagent, 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 revdetails.createManifestDialog(self._repoagent, self.rev) def mergeWithOtherHead(self): """Open dialog to merge with the other head of the current branch""" cmdline = hglib.buildcmdargs('merge', preview=True, config=r'ui.logtemplate={rev}\n') sess = self._runCommand(cmdline) sess.setCaptureOutput(True) sess.commandFinished.connect(self._onMergePreviewFinished) @pyqtSlot(int) def _onMergePreviewFinished(self, ret): sess = self.sender() if ret == 255 and 'hg heads' in sess.errorString(): # multiple heads self.filterbar.setQuery('head() - .') self.filterbar.runQuery() msg = '\n'.join(sess.errorString().splitlines()[:-1]) # drop hint w = self.setInfoBar(infobar.ConfirmInfoBar, msg) assert w w.acceptButton.setText(_('Merge')) w.accepted.connect(self.mergeWithRevision) w.finished.connect(self.clearRevisionSet) return if ret != 0: return revs = map(int, str(sess.readAll()).splitlines()) if not revs: return self._dialogs.open(RepoWidget._createMergeDialog, revs[-1]) @pyqtSlot() 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): msg = backout.checkrev(self._repoagent.rawRepo(), self.rev) if msg: qtlib.InfoMsgBox(_('Unable to backout'), msg, parent=self) return dlg = backout.BackoutDialog(self._repoagent, self.rev, self) dlg.finished.connect(dlg.deleteLater) dlg.exec_() @pyqtSlot() def _pruneSelected(self): revspec = hglib.compactrevs(sorted(self.repoview.selectedRevisions())) dlg = prune.createPruneDialog(self._repoagent, revspec, self) dlg.exec_() def stripRevision(self): 'Strip the selected revision and all descendants' dlg = thgstrip.createStripDialog(self._repoagent, rev=str(self.rev), parent=self) 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.createRemoteUpdateDialog(self._repoagent, self.rev, self) 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.createArchiveDialog(self._repoagent, rev, self) dlg.exec_() def archiveRevisions(self, revs): rev = max(revs) minrev = min(revs) dlg = archive.createArchiveDialog(self._repoagent, rev=rev, minrev=minrev, parent=self) 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, _filter = QFileDialog.getSaveFileName( self, _('Write bundle'), os.path.join(root, filename)) if not file: return cmdline = ['bundle', '--verbose'] parents = [hglib.escaperev(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 if self.rev is None: command = 'diff' else: command = 'export' assert command in ('export', 'diff') if command == 'export': # patches should be in chronological order revs = sorted(self.menuselection) cmdline = hglib.buildcmdargs('export', rev=hglib.compactrevs(revs)) else: revs = self.rev and self.menuselection or None cmdline = hglib.buildcmdargs('diff', rev=revs) return self._runCommand(cmdline) @pyqtSlot() def copyPatch(self): sess = self._buildPatch() sess.setCaptureOutput(True) sess.commandFinished.connect(self._copyPatchOutputToClipboard) @pyqtSlot(int) def _copyPatchOutputToClipboard(self, ret): if ret == 0: sess = self.sender() output = sess.readAll() mdata = QMimeData() mdata.setData('text/x-diff', output) # for lossless import mdata.setText(hglib.tounicode(str(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 = action.data() 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 revList = self.repo.revs('%s::%s and not hidden()' % (self.rev, endrev)) 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.vfs.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""" self._mqActions.finishRevision(hglib.tounicode(str(self.rev))) @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 = [] selection = self.repoview.selectedRevisions() def selectionfiles2str(source): files = set() for rev in selection: files.update(f for f in getattr(self.repo[rev], source)()) return filelist2str(sorted(files)) vars = { 'ROOT': lambda: self.repo.root, 'REVID': lambda: '+'.join(str(self.repo[rev]) for rev in selection), 'REV': lambda: '+'.join(str(rev) for rev in selection), 'FILES': lambda: selectionfiles2str('files'), 'ALLFILES': lambda: selectionfiles2str('manifest'), 'SELECTEDFILES': lambda: filelist2str(files), } if len(selection) == 2: pairvars = { 'REV_A': lambda: selection[0], 'REV_B': lambda: selection[1], 'REVID_A': lambda: str(self.repo[selection[0]]), 'REVID_B': lambda: str(self.repo[selection[1]]), } vars.update(pairvars) for var in vars: bracedvar = '{%s}' % var if bracedvar in command: command = command.replace(bracedvar, str(vars[var]())) if workingdir and bracedvar in workingdir: workingdir = workingdir.replace(bracedvar, 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 = hglib.loadui() 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 . 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, shell=True) 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() 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) self._handleNewCommand(sess) return sess def _runCommandSequence(self, cmdlines): sess = self._repoagent.runCommandSequence(cmdlines, self) self._handleNewCommand(sess) return sess def _handleNewCommand(self, sess): self.clearInfoBar() sess.outputReceived.connect(self._repoviewFrame.showOutput) @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._namedTabs['commit']: return refreshwd = self.repo.ui.config('tortoisehg', 'refreshwdstatus', 'auto') # Valid refreshwd values are 'auto', 'always' and 'alwayslocal' if refreshwd != 'auto': if refreshwd == 'always' \ or paths.is_on_fixed_drive(self.repo.root): self.commitDemand.forward('refreshWctx') class LightRepoWindow(QMainWindow): def __init__(self, repoagent): super(LightRepoWindow, self).__init__() self._repoagent = repoagent self.setIconSize(qtlib.smallIconSize()) repo = repoagent.rawRepo() val = repo.ui.config('tortoisehg', 'tasktabs', 'off').lower() if val not in ('east', 'west'): repo.ui.setconfig('tortoisehg', 'tasktabs', 'east') rw = RepoWidget(repoagent, self) self.setCentralWidget(rw) self._edittbar = tbar = self.addToolBar(_('&Edit Toolbar')) tbar.setObjectName('edittbar') a = tbar.addAction(qtlib.geticon('view-refresh'), _('&Refresh')) a.setShortcuts(QKeySequence.Refresh) a.triggered.connect(self.refresh) tbar = rw.filterBar() tbar.setObjectName('filterbar') tbar.setWindowTitle(_('&Filter Toolbar')) self.addToolBar(tbar) stbar = cmdui.ThgStatusBar(self) repoagent.progressReceived.connect(stbar.setProgress) rw.showMessageSignal.connect(stbar.showMessage) rw.progress.connect(stbar.progress) self.setStatusBar(stbar) s = QSettings() s.beginGroup('LightRepoWindow') self.restoreGeometry(qtlib.readByteArray(s, 'geometry')) self.restoreState(qtlib.readByteArray(s, 'windowState')) stbar.setVisible(qtlib.readBool(s, 'statusBar', True)) s.endGroup() self.setWindowTitle(_('TortoiseHg: %s') % repoagent.displayName()) def createPopupMenu(self): menu = super(LightRepoWindow, self).createPopupMenu() assert menu # should have toolbar stbar = self.statusBar() a = menu.addAction(_('S&tatus Bar')) a.setCheckable(True) a.setChecked(stbar.isVisibleTo(self)) a.triggered.connect(stbar.setVisible) menu.addSeparator() menu.addAction(_('&Settings'), self._editSettings) return menu def closeEvent(self, event): rw = self.centralWidget() if not rw.closeRepoWidget(): event.ignore() return s = QSettings() s.beginGroup('LightRepoWindow') s.setValue('geometry', self.saveGeometry()) s.setValue('windowState', self.saveState()) s.setValue('statusBar', self.statusBar().isVisibleTo(self)) s.endGroup() event.accept() @pyqtSlot() def refresh(self): self._repoagent.pollStatus() rw = self.centralWidget() rw.reload() def setSyncUrl(self, url): rw = self.centralWidget() rw.setSyncUrl(url) @pyqtSlot() def _editSettings(self): dlg = settings.SettingsDialog(parent=self) dlg.exec_() tortoisehg-4.5.2/tortoisehg/hgqt/__init__.py0000644000175000017500000000000013150123225021746 0ustar sborhosborho00000000000000tortoisehg-4.5.2/tortoisehg/__init__.py0000644000175000017500000000001513150123225021011 0ustar sborhosborho00000000000000#placeholder tortoisehg-4.5.2/PKG-INFO0000644000175000017500000000037213251112740015616 0ustar sborhosborho00000000000000Metadata-Version: 1.0 Name: tortoisehg Version: 4.5.2 Summary: TortoiseHg dialogs for Mercurial VCS Home-page: https://tortoisehg.bitbucket.io Author: Steve Borho Author-email: steve@borho.org License: GNU GPL2 Description: UNKNOWN Platform: UNKNOWN tortoisehg-4.5.2/i18n/0000755000175000017500000000000013251112740015276 5ustar sborhosborho00000000000000tortoisehg-4.5.2/i18n/msgfmt.py0000644000175000017500000002005213150123225017142 0ustar sborhosborho00000000000000#! /usr/bin/env python # -*- coding: iso-8859-1 -*- # Written by Martin v. Loewis # # Changed by Christian 'Tiran' Heimes for the placeless # translation service (PTS) of Zope # # Fixed some bugs and updated to support msgctxt # by Hanno Schlichting """Generate binary message catalog from textual translation description. This program converts a textual Uniforum-style message catalog (.po file) into a binary GNU catalog (.mo file). This is essentially the same function as the GNU msgfmt program, however, it is a simpler implementation. This file was taken from Python-2.3.2/Tools/i18n and altered in several ways. Now you can simply use it from another python module: from msgfmt import Msgfmt mo = Msgfmt(po).get() where po is path to a po file as string, an opened po file ready for reading or a list of strings (readlines of a po file) and mo is the compiled mo file as binary string. Exceptions: * IOError if the file couldn't be read * msgfmt.PoSyntaxError if the po file has syntax errors """ import struct import array from cStringIO import StringIO __version__ = "1.1-pythongettext" class PoSyntaxError(Exception): """ Syntax error in a po file """ def __init__(self, msg): self.msg = msg def __str__(self): return 'Po file syntax error: %s' % self.msg class Msgfmt: """ """ def __init__(self, po, name='unknown'): self.po = po self.name = name self.messages = {} self.openfile = False def readPoData(self): """ read po data from self.po and return an iterator """ output = [] if isinstance(self.po, str): output = open(self.po, 'rb') elif isinstance(self.po, file): self.po.seek(0) self.openfile = True output = self.po elif isinstance(self.po, list): output = self.po if not output: raise ValueError, "self.po is invalid! %s" % type(self.po) return output def add(self, context, id, str, fuzzy): "Add a non-empty and non-fuzzy translation to the dictionary." if str and not fuzzy: # The context is put before the id and separated by a EOT char. if context: id = context + '\x04' + id self.messages[id] = str def generate(self): "Return the generated output." keys = self.messages.keys() # the keys are sorted in the .mo file keys.sort() offsets = [] ids = strs = '' for id in keys: # For each string, we need size and file offset. Each string is # NUL terminated; the NUL does not count into the size. offsets.append((len(ids), len(id), len(strs), len(self.messages[id]))) ids += id + '\0' strs += self.messages[id] + '\0' output = '' # The header is 7 32-bit unsigned integers. We don't use hash tables, # so the keys start right after the index tables. keystart = 7*4+16*len(keys) # and the values start after the keys valuestart = keystart + len(ids) koffsets = [] voffsets = [] # The string table first has the list of keys, then the list of values. # Each entry has first the size of the string, then the file offset. for o1, l1, o2, l2 in offsets: koffsets += [l1, o1+keystart] voffsets += [l2, o2+valuestart] offsets = koffsets + voffsets # Even though we don't use a hashtable, we still set its offset to be # binary compatible with the gnu gettext format produced by: # msgfmt file.po --no-hash output = struct.pack("Iiiiiii", 0x950412deL, # Magic 0, # Version len(keys), # # of entries 7*4, # start of key index 7*4+len(keys)*8, # start of value index 0, keystart) # size and offset of hash table output += array.array("i", offsets).tostring() output += ids output += strs return output def get(self): """ """ self.read() # Compute output return self.generate() def read(self, header_only=False): """ """ ID = 1 STR = 2 CTXT = 3 section = None fuzzy = 0 msgid = msgstr = msgctxt = '' # Parse the catalog lno = 0 for l in self.readPoData(): lno += 1 # If we get a comment line after a msgstr or a line starting with # msgid or msgctxt, this is a new entry if section == STR and (l[0] == '#' or (l[0] == 'm' and (l.startswith('msgctxt') or l.startswith('msgid')))): self.add(msgctxt, msgid, msgstr, fuzzy) section = None msgctxt = '' fuzzy = 0 # If we only want the header we stop after the first message if header_only: break # Record a fuzzy mark if l[:2] == '#,' and 'fuzzy' in l: fuzzy = 1 # Skip comments if l[0] == '#': continue # Now we are in a msgctxt section elif l[0] == 'm': if l.startswith('msgctxt'): section = CTXT l = l[7:] msgctxt = '' # Now we are in a msgid section, output previous section elif l.startswith('msgid') and not l.startswith('msgid_plural'): if section == STR: self.add(msgid, msgstr, fuzzy) section = ID l = l[5:] msgid = msgstr = '' is_plural = False # This is a message with plural forms elif l.startswith('msgid_plural'): if section != ID: print >> sys.stderr, 'msgid_plural not preceeded by msgid on %s:%d' %\ (infile, lno) sys.exit(1) l = l[12:] msgid += '\0' # separator of singular and plural is_plural = True # Now we are in a msgstr section elif l.startswith('msgstr'): section = STR if l.startswith('msgstr['): if not is_plural: print >> sys.stderr, 'plural without msgid_plural on %s:%d' %\ (infile, lno) sys.exit(1) l = l.split(']', 1)[1] if msgstr: msgstr += '\0' # Separator of the various plural forms else: if is_plural: print >> sys.stderr, 'indexed msgstr required for plural on %s:%d' %\ (infile, lno) sys.exit(1) l = l[6:] # Skip empty lines l = l.strip() if not l: continue # XXX: Does this always follow Python escape semantics? try: l = eval(l) except Exception, msg: raise PoSyntaxError('%s (line %d of po file %s): \n%s' % (msg, lno, self.name, l)) if section == CTXT: msgctxt += l elif section == ID: msgid += l elif section == STR: msgstr += l else: raise PoSyntaxError('error in line %d of po file %s' % (lno, self.name)) # Add last entry if section == STR: self.add(msgctxt, msgid, msgstr, fuzzy) if self.openfile: self.po.close() def getAsFile(self): return StringIO(self.get()) tortoisehg-4.5.2/i18n/tortoisehg/0000755000175000017500000000000013251112740017465 5ustar sborhosborho00000000000000tortoisehg-4.5.2/i18n/tortoisehg/ar.po0000644000175000017500000043010513251112733020434 0ustar sborhosborho00000000000000# Arabic translation for tortoisehg # Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011 # This file is distributed under the same license as the tortoisehg package. # FIRST AUTHOR , 2011. # msgid "" msgstr "" "Project-Id-Version: tortoisehg\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2017-11-28 15:25-0200\n" "PO-Revision-Date: 2011-10-25 07:12+0000\n" "Last-Translator: Fadi Mansour \n" "Language-Team: Arabic \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n % 100 >= " "3 && n % 100 <= 10 ? 3 : n % 100 >= 11 && n % 100 <= 99 ? 4 : 5;\n" "X-Launchpad-Export-Date: 2017-11-29 05:17+0000\n" "X-Generator: Launchpad (build 18511)\n" msgid "TortoiseHg Overlay Icon Server" msgstr "й…иЎиЏй… иЃйŠй‚йˆй†иЇиЊ иЇй„иЏй„иЇй„иЉ й„иЊйˆиБиЊйˆйŠиВ иЅиЊиД иЌйŠ" msgid "Exit" msgstr "иЅй†й‡иЇиЁ" msgid "About" msgstr "и­йˆй„ иЇй„иЈиБй†иЇй…иЌ" msgid "Copyright 2008-2017 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 "&иБиЎиЕиЉ" #, python-format msgid "version %s" msgstr "иЇй„й†иГиЎиЉ %s" #, python-format msgid "with Mercurial-%s, Python-%s, PyQt-%s, Qt-%s" msgstr "иЈиЇиГиЊиЎиЏиЇй… Mercurial-%s, Python-%s, PyQt-%s, Qt-%s" #, python-format msgid "A new version of TortoiseHg (%s) is ready for download!" msgstr "" msgid "License" msgstr "" msgid "= Working Directory Parent =" msgstr "=иЃиЈ й…иЌй„иЏ иЇй„иЙй…й„=" msgid "Directory of files" msgstr "й‚иЇиІй…иЉ иЇй„й…й„йиЇиЊ" msgid "Tar archives" msgstr "" msgid "Uncompressed tar archive" msgstr "иЃиБиДйŠй tar иКйŠиБ й…иЖиКйˆиЗ" msgid "Bzip2 tar archives" msgstr "" msgid "Tar archive compressed using bzip2" msgstr "иЃиБиДйŠй tar й…иЖиКйˆиЗ иЈй€bzip2" msgid "Gzip tar archives" msgstr "" msgid "Tar archive compressed using gzip" msgstr "иЃиБиДйŠй tar й…иЖиКйˆиЗ иЈй€gzip" msgid "Zip archives" msgstr "" msgid "Uncompressed zip archive" msgstr "иЃиБиДйŠй zip иКйŠиБ й…иЖййˆиЗ" msgid "Zip archive compressed using deflate" msgstr "иЃиБиДйŠй zip й…иЖййˆиЗ иЈй€deflate" msgid "Revision:" msgstr "иЇй„й…иБиЇиЌиЙиЉ:" msgid "All files in this revision" msgstr "" msgid "Only files modified/created in this revision" msgstr "йй‚иЗ иЇй„й…й„йиЇиЊ иЇй„й…иЙиЏй„иЉ/иЇй„й…й†иДиЃиЉ ййŠ й‡иАй‡ иЇй„й…иБиЇиЌиЙиЉ" msgid "Only files modified/created since:" msgstr "" msgid "Archive Content:" msgstr "" msgid "Recurse into subrepositories" msgstr "иЇй„й†иВйˆй„ й„й„й…иЌй„иЏиЇиЊ иЇй„йиБиЙйŠиЉ" msgid "Browse..." msgstr "иЊиЕйй‘и­..." msgid "Destination path:" msgstr "й…иГиЇиБ иЇй„й‡иЏй:" msgid "Archive types:" msgstr "иЃй†йˆиЇиЙ иЇй„иЃиБиДйŠйиЇиЊ:" msgid "Hg command:" msgstr "иЃй…иБ hg:" msgid "Select Destination Folder" msgstr "" msgid "Select Destination File" msgstr "" msgid "All files (*)" msgstr "" msgid "Duplicate Name" msgstr "" #, python-format msgid "The destination \"%s\" already exists as a file!" msgstr "" msgid "Confirm Overwrite" msgstr "" #, 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 "Archive - %s" msgstr "" msgid "&Archive" msgstr "" msgid "Backout requires a parent revision" msgstr "" msgid "Cannot backout change on a different branch" 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 "Backing out a parent revision is a single step operation" msgstr "" msgid "Backout revision" msgstr "" msgid "Not a head, backout will create a new head!" msgstr "" msgid "Current local 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 "" 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 changeset: " msgstr "" msgid "Confirm Discard Message" msgstr "" msgid "Discard current backout message?" msgstr "" msgid "Use English backout message" 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 "Accept" msgstr "" msgid "Known good revision:" msgstr "" msgid "Known bad revision:" msgstr "" msgid "Discard local changes (revert --all)" msgstr "" msgid "Revision is &Good" msgstr "" msgid "Revision is &Bad" msgstr "" msgid "&Skip this Revision" msgstr "" msgid "Close" msgstr "" 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 "" msgid "New Name:" msgstr "" 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 "" #, python-format msgid "Bookmark '%s' has been removed" msgstr "" #, python-format msgid "Bookmark '%s' has been renamed to '%s'" msgstr "" msgid "TortoiseHg Bookmark Sync" msgstr "" msgid "Outgoing Bookmarks" msgstr "" msgid "&Push Bookmark" msgstr "" msgid "&Remove Bookmark" msgstr "" msgid "Incoming Bookmarks" msgstr "" msgid "P&ull Bookmark" msgstr "" msgid "R&emove Bookmark" msgstr "" #, python-format msgid "Pushed local bookmark: %s" msgstr "" #, python-format msgid "Pulled remote bookmark: %s" msgstr "" #, python-format msgid "Removed remote bookmark: %s" msgstr "" #, python-format msgid "Removed local bookmark: %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 "A revision identifier, bookmark, tag or branch name" msgstr "" msgid "Do not update the new working directory" msgstr "" msgid "Use pull protocol to copy metadata" msgstr "" msgid "Use uncompressed transfer" msgstr "" msgid "Include patch queue" msgstr "" msgid "Use proxy server" msgstr "" msgid "Do not verify host certificate" msgstr "" msgid "Remote command:" msgstr "" msgid "Use largefiles" msgstr "" msgid "Start revision:" msgstr "" msgid "Select source repository" msgstr "" msgid "Select destination repository" msgstr "" msgid "Select patch folder" msgstr "" #, python-format msgid "Clone - %s" msgstr "" msgid "&Clone" msgstr "" msgid "failed to start command\n" msgstr "" msgid "error while running command\n" msgstr "" #, python-format msgid "process exited unexpectedly with code %d" msgstr "" #, python-format msgid "failed to encode command: %s" msgstr "" #, python-format msgid "timed out while reading: %r..." msgstr "" msgid "timed out waiting for message" msgstr "" #, python-format msgid "unexpected response on required channel %r" msgstr "" #, python-format msgid "invalid \"hello\" message: %r" msgstr "" msgid "no \"runcommand\" capability" msgstr "" #, python-format msgid "corrupted command result: %r" msgstr "" #, python-format msgid "failed to encode input: %s" 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 Prompt" msgstr "" msgid "Show Detail" msgstr "" msgid "Hide Detail" msgstr "" msgid "Confirm Exit" msgstr "" msgid "" "Mercurial command is still running.\n" "Are you sure you want to terminate?" msgstr "" msgid "&Run" msgstr "" msgid "TortoiseHg Command Dialog" msgstr "" msgid "Command Error" msgstr "" #, python-format msgid "[Code: %d]" msgstr "" msgid "Merge" msgstr "" #, python-format msgid "Merge with %s" msgstr "" msgid "Patch Name Required" msgstr "" msgid "You must enter a patch name" 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 "### patch name ###" msgstr "" msgid "Commit changes" msgstr "" msgid "Commit" msgstr "" msgid "Amend current revision" msgstr "" msgid "Amend" msgstr "" msgid "Create a new patch" msgstr "" msgid "QNew" msgstr "" msgid "Refresh current patch" msgstr "" msgid "QRefresh" msgstr "" 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 "Cancel" msgstr "" msgid "Confirm New Branch" msgstr "" #, 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 "" msgid "Message Translation Failure" msgstr "" msgid "" "Unable to translate message to local encoding.\n" "Consider setting HGENCODING environment variable.\n" "\n" "Replace untranslatable characters with \"?\"?\n" msgstr "" msgid "&Replace" msgstr "" msgid "Nothing Committed" msgstr "" msgid "Please enter commit message" msgstr "" 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 "" msgid "Nothing changed." 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, shelve to patch, 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 "" #, 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 "" #, python-brace-format 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" "Common variables:\n" "- {ROOT}: The path to the current repository root.\n" "- {REV} / {REVID}: Selected revisions numbers / hexadecimal revision id " "hashes respectively formatted as a revset expression.\n" "- {SELECTEDFILES}: The list of files selected by the user on the revision " "details file list.\n" "- {FILES}: The list of files touched by the selected revisions.\n" "- {ALLFILES}: All the files tracked by Mercurial on the selected revisions.\n" "Pair selection variables:\n" "- {REV_A} / {REVID_A}: the first selected revision number / hexadecimal " "revision id hash respectively.\n" "- {REV_B} / {REVID_B}: the second selected revision number / hexadecimal " "revision id hash respectively.\n" msgstr "" #, python-brace-format 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 "" "Run after a changegroup has been added via push, pull or unbundle. ID of the " "first new changeset is in $HG_NODE and last in $HG_NODE_LAST. URL from which changes came is in $HG_URL." msgstr "" msgid "" "Run after a changeset has been created in the local repository. ID of the " "newly created changeset is in $HG_NODE. Parent changeset IDs are in " "$HG_PARENT1 and $HG_PARENT2." msgstr "" msgid "" "Run after a changeset has been pulled, pushed, or unbundled into the local " "repository. The ID of the newly arrived changeset is in $HG_NODE. " "URL that was source of changes came is in $HG_URL." msgstr "" msgid "" "Run after sending changes from local repository to another. ID of first " "changeset sent is in $HG_NODE. Source of operation is in " "$HG_SOURCE." msgstr "" msgid "" "Run before a changegroup is added via push, pull or unbundle. Exit status 0 " "allows the changegroup to proceed. Non-zero status will cause the push, pull " "or unbundle to fail. URL from which changes will come is in $HG_URL." msgstr "" msgid "" "Run before starting a local commit. Exit status 0 allows the commit to " "proceed. Non-zero status will cause the commit to fail. Parent changeset IDs " "are in $HG_PARENT1 and $HG_PARENT2." msgstr "" msgid "" "Run before listing pushkeys (like bookmarks) in the repository. Non-zero " "status will cause failure. The key namespace is in $HG_NAMESPACE." msgstr "" msgid "" "Run before collecting changes to send from the local repository to another. " "Non-zero status will cause failure. This lets you prevent pull over HTTP or " "SSH. Also prevents against local pull, push (outbound) or bundle commands, " "but not effective, since you can just copy files instead then. Source of " "operation is in $HG_SOURCE. If \"serve\", operation is happening on " "behalf of remote SSH or HTTP repository. If \"push\", \"pull\" or \"bundle" "\", operation is happening on behalf of repository on same system." msgstr "" msgid "" "Run before a pushkey (like a bookmark) is added to the repository. Non-zero " "status will cause the key to be rejected. The key namespace is in " "$HG_NAMESPACE, the key is in $HG_KEY, the old value (if any) " "is in $HG_OLD, and the new value is in $HG_NEW." msgstr "" msgid "" "Run before creating a tag. Exit status 0 allows the tag to be created. Non-" "zero status will cause the tag to fail. ID of changeset to tag is in " "$HG_NODE. Name of tag is in $HG_TAG. Tag is local if " "$HG_LOCAL=1, in repository if $HG_LOCAL=0." msgstr "" msgid "" "Run after a changegroup has been added via push, pull or unbundle, but " "before the transaction has been committed. Changegroup is visible to hook " "program. This lets you validate incoming changes before accepting them. " "Passed the ID of the first new changeset in $HG_NODE and last in " "$HG_NODE_LAST. Exit status 0 allows the transaction to commit. Non-" "zero status will cause the transaction to be rolled back and the push, pull " "or unbundle will fail. URL that was source of changes is in $HG_URL." msgstr "" msgid "" "Run after a changeset has been created but the transaction not yet " "committed. Changeset is visible to hook program. This lets you validate " "commit message and changes. Exit status 0 allows the commit to proceed. Non-" "zero status will cause the transaction to be rolled back. ID of changeset is " "in $HG_NODE. Parent changeset IDs are in $HG_PARENT1 and " "$HG_PARENT2." msgstr "" msgid "" "Run before updating the working directory. Exit status 0 allows the update " "to proceed. Non-zero status will prevent the update. Changeset ID of first " "new parent is in $HG_PARENT1. If merge, ID of second new parent is " "in $HG_PARENT2." msgstr "" msgid "" "Run after listing pushkeys (like bookmarks) in the repository. The key " "namespace is in $HG_NAMESPACE. $HG_VALUES is a dictionary " "containing the keys and values." msgstr "" msgid "" "Run after a pushkey (like a bookmark) is added to the repository. The key " "namespace is in $HG_NAMESPACE, the key is in $HG_KEY, the " "old value (if any) is in $HG_OLD, and the new value is in " "$HG_NEW." msgstr "" msgid "" "Run after a tag is created. ID of tagged changeset is in $HG_NODE. " "Name of tag is in $HG_TAG. Tag is local if $HG_LOCAL=1, in " "repository if $HG_LOCAL=0." msgstr "" msgid "" "Run after updating the working directory. Changeset ID of first new parent " "is in $HG_PARENT1. If merge, ID of second new parent is in " "$HG_PARENT2. If the update succeeded, $HG_ERROR=0. If the " "update failed (e.g. because conflicts not resolved), $HG_ERROR=1." 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 "Console" msgstr "" msgid "File &History / Annotate" msgstr "" msgid "Show the history of the selected file" msgstr "" msgid "Co&mpare File Revisions" msgstr "" msgid "Compare revisions of the selected file" msgstr "" msgid "Filter Histor&y" msgstr "" msgid "Query about changesets affecting the selected files" msgstr "" msgid "Diff &Changeset to Parent" msgstr "" msgid "Diff Changeset to Loc&al" 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 "Save file to" msgstr "" msgid "&Edit Local" msgstr "" msgid "&Open Local" msgstr "" msgid "E&xplore Local" msgstr "" msgid "Open parent folder of current file in the system file manager" msgstr "" msgid "&Copy Patch" 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 "Custom Tools" msgstr "" msgid "Diff &Local" msgstr "" msgid "&View Missing" msgstr "" msgid "View O&ther" msgstr "" msgid "Add &Largefiles..." msgstr "" msgid "&Forget" msgstr "" msgid "&Delete Unversioned..." msgstr "" msgid "Confirm Delete Unversioned" msgstr "" msgid "Delete the following unversioned files?" msgstr "" msgid "&Delete" msgstr "" msgid "Re&move Versioned" msgstr "" msgid "&Revert..." msgstr "" msgid "Uncommited merge - please select a parent revision" msgstr "" msgid "Revert files to local or other parent?" msgstr "" msgid "&Local" msgstr "" msgid "&Other" msgstr "" msgid "Confirm Revert" msgstr "" msgid "Revert local file changes?" msgstr "" msgid "&Revert with backup" msgstr "" msgid "&Discard changes" msgstr "" msgid "Revert the following files?" msgstr "" msgid "&Revert" msgstr "" msgid "&Copy..." msgstr "" msgid "Re&name..." msgstr "" msgid "&Ignore..." msgstr "" msgid "Edit Re&jects" msgstr "" msgid "Manually resolve rejected patch chunks" msgstr "" msgid "De&tect Renames..." msgstr "" msgid "&Mark Resolved" msgstr "" msgid "&Mark Unresolved" msgstr "" msgid "Restart Mer&ge" msgstr "" msgid "Was renamed from" msgstr "" msgid "Restart Merge &with" msgstr "" msgid "Display the file anyway" msgstr "" msgid "Diff 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 "" msgid "File or diffs not displayed: " msgstr "" msgid " (was added)" msgstr "" #, python-format msgid " (copied from %s)" msgstr "" #, python-format msgid " (renamed from %s)" msgstr "" msgid " (is a symlink)" 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 "" msgid "exec mode has been set" msgstr "" msgid "exec mode has been unset" 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 "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 "Show Revision &Details" msgstr "" msgid "Too many rows selected for menu" msgstr "" msgid "File Differences Log Columns" msgstr "" msgid "Next diff" msgstr "" msgid "Previous diff" msgstr "" msgid "Unicode" msgstr "" msgid "Western Europe" msgstr "" msgid "Unified Chinese" msgstr "" msgid "Traditional Chinese" msgstr "" msgid "Korean" msgstr "" msgid "Japanese" msgstr "" msgid "Thai" msgstr "" msgid "Central and Eastern Europe" msgstr "" msgid "Cyrillic" msgstr "" msgid "Russian" msgstr "" msgid "Ukrainian" msgstr "" msgid "Greek" msgstr "" msgid "Turkish" msgstr "" msgid "Arabic" msgstr "" msgid "Hebrew" msgstr "" msgid "Vietnamese" msgstr "" msgid "Baltic" msgstr "" msgid "Southern Europe" msgstr "" msgid "Nordic" msgstr "" msgid "Celtic" msgstr "" msgid "South-Eastern Europe" msgstr "" #. i18n: comma-separated list of common encoding names in your locale, e.g. #. "utf-8,shift_jis,euc_jp,iso2022_jp" for "ja" locale. #. #. for the best guess, put structured encodings like "utf-8" in front, e.g. #. "utf-8,iso8859-1" instead of "iso8859-1,utf-8" because "iso8859-1" can #. decode arbitrary byte sequence and never fall back. #. #. pick from the following encodings: #. utf-8, iso8859-1, cp1252, gbk, big5, big5hkscs, euc_kr, cp932, euc_jp, #. iso2022_jp, cp874, iso8859-15, mac-roman, iso8859-2, cp1250, iso8859-5, #. cp1251, koi8-r, koi8-u, iso8859-7, cp1253, cp1254, cp1256, iso8859-6, #. cp1255, iso8859-8, cp1258, iso8859-4, iso8859-13, cp1257, iso8859-3, #. iso8859-10, iso8859-14, iso8859-16 msgid "$FILE_ENCODINGS" 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" msgstr "" msgid "Previous Diff" msgstr "" msgid "Open shelve tool" msgstr "" msgid "&Auto Detect" msgstr "" msgid "Show changes from first parent" msgstr "" msgid "Show changes from second parent" msgstr "" msgid "E&ncoding" msgstr "" msgid "&Search in Current File" msgstr "" msgid "Search in All &History" msgstr "" msgid "Go to Line" msgstr "" #, python-format msgid "Enter line number (1 - %d)" msgstr "" msgid "Show &Author" msgstr "" msgid "Show &Date" msgstr "" msgid "Show &Revision" 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 "&Parent Revision" msgstr "" #, python-format msgid "&Parent Revision (%d)" msgstr "" msgid "&Mark Excluded Changes" msgstr "" msgid "(excluded from the next commit)" 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 "" 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, shelve to patch, or discard " "changes." msgstr "" msgid "You may continue or start the graft" msgstr "" msgid "Graft is complete" msgstr "" msgid "Graft failed" msgstr "" msgid "Graft aborted" 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 "" #, 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 "" msgid "TortoiseHg Search" 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 "" msgid "&Close" msgstr "иЅ&иКй„иЇй‚" #, python-format msgid "Ignore filter - %s" msgstr "" msgid "Glob" msgstr "" msgid "Regexp" msgstr "" msgid "Add" msgstr "" 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 "Copy working directory files from skeleton" msgstr "" msgid "Create special files (.hgignore, ...)" msgstr "" msgid "Make repo compatible with Mercurial <1.7" msgstr "" msgid "New Repository" msgstr "" msgid "&Create" msgstr "" msgid "Unable to create a config file" msgstr "" msgid "Insufficient access rights." msgstr "" msgid "Show Log" 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 "Invalid Patterns" msgstr "" msgid "Failed to process largefiles.patterns." msgstr "" msgid "Word docs (*.doc *.docx)" msgstr "" msgid "PDF docs (*.pdf)" msgstr "" msgid "Excel files (*.xls *.xlsx)" msgstr "" #, python-format msgid "TortoiseHg Lock Tool - %s" msgstr "" msgid "Refresh lock information" msgstr "" msgid "Lock a file not described in .hglocks" msgstr "" msgid "Stop current operation" msgstr "" msgid "Locked And Lockable Files:" msgstr "" msgid "Path" msgstr "" msgid "Locking User" msgstr "" msgid "Purpose" msgstr "" msgid "Simplelock extension not enabled" msgstr "" msgid "Please enable and configure simplelock" msgstr "" msgid "Open a (nonmergable) file you wish to be locked" msgstr "" msgid "File was not within current repository" msgstr "" #, python-format msgid "Locking %s" msgstr "" #, python-format msgid "Unlocking %s" msgstr "" msgid "Refreshing locks..." msgstr "" #, python-format msgid "Lock of %s successful" msgstr "" #, python-format msgid "Lock of %s failed, retry" msgstr "" #, python-format msgid "Unlock of %s failed, retry" msgstr "" #, python-format msgid "Unlock of %s successful" msgstr "" msgid "Ready, double click to lock or unlock" msgstr "" msgid "Ready" msgstr "" msgid "You can only release your own locks" 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 "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 the 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 "" "No merge conflicts, ready to commit or review" msgstr "" msgid "Commit merge results" msgstr "" msgid "Commit Options" msgstr "" msgid "Commit Now" msgstr "" msgid "Commit Later" msgstr "" msgid "Merge changeset" msgstr "" msgid "Syntax Highlighting" msgstr "" msgid "Paste &Filenames" msgstr "" msgid "App&ly Format" msgstr "" msgid "C&onfigure Format" msgstr "" #, python-format msgid "%s had rejected chunks, edit patched file together with rejects?" 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 "Create" msgstr "" msgid "Rename Patch Queue" msgstr "" #, python-format msgid "Rename patch queue '%s' to" msgstr "" msgid "Rename" msgstr "" 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 "" msgctxt "MQ QPush" msgid "Push" msgstr "" msgid "Apply one patch" msgstr "" msgctxt "MQ QPush" msgid "Push all" msgstr "" msgid "Apply all patches" msgstr "" msgid "Pop" msgstr "" msgid "Unapply one patch" msgstr "" msgid "Pop all" msgstr "" msgid "Unapply all patches" msgstr "" msgid "Go &to Patch" msgstr "" msgid "&Finish Patch" msgstr "" msgid "Move applied patches into repository history" msgstr "" msgid "&Delete Patches..." msgstr "" msgid "Delete selected patches" msgstr "" msgid "Re&name Patch..." msgstr "" msgid "Set &Guards..." msgstr "" msgid "Configure guards for selected 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 "" #, python-format msgid "Pending Perforce Changelists - %s" msgstr "" msgid "Submitting p4 changelist..." msgstr "" msgid "Reverting p4 changelist..." 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 "Status" 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 "Target:" msgstr "" msgid "Do not modify working copy (-k/--keep)" msgstr "" #, python-format msgid "Prune - %s" msgstr "" msgid "&Prune" 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 "&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 file" msgstr "" msgid "Could not open the specified file for reading." msgstr "" msgid "This appears to be a binary file." msgstr "" msgid "An error occurred while reading the file." msgstr "" msgid "Text Translation Failure" msgstr "" msgid "Could not translate the file content from native encoding." msgstr "" msgid "Several characters would be lost." msgstr "" msgid "Unable to write file" msgstr "" msgid "Could not translate the file content to native encoding." msgstr "" msgid "Could not open the specified file for writing." msgstr "" msgid "An error occurred while writing the 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 "Repository Error" msgstr "" msgid "No visual editor configured" msgstr "" msgid "Please configure a visual editor." msgstr "" msgid "Editor launch failure" msgstr "" msgid "Failed to open path in terminal" msgstr "" #, python-format msgid "\"%s\" is not a valid directory" msgstr "" #, python-format msgid "Invalid configuration: %s" msgstr "" msgid "Unable to start the following command:" msgstr "" msgid "No shell configured" msgstr "" msgid "A terminal shell must be configured" msgstr "" msgid "Please enter a username" msgstr "" msgid "You must identify yourself to Mercurial" 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 "" #, 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 (--keep)" msgstr "" msgid "Keep original branch names (--keepbranches)" msgstr "" msgid "Collapse the rebased changesets (--collapse)" msgstr "" msgid "Rebase entire source branch (-b/--base)" 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, shelve to patch, or discard changes." msgstr "" msgid "You may continue the rebase" msgstr "" msgid "Rebase is complete" msgstr "" msgid "Rebase failed" msgstr "" msgid "Rebase aborted" 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 "Reload File" msgstr "" msgid "Are you sure you want to reload this file?" msgstr "" msgid "All unsaved changes will be lost." 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 "" msgid "Copy Error" msgstr "" msgid "Select Source File" msgstr "" msgid "Select Source Folder" msgstr "" msgid "Source does not exist." msgstr "" msgid "The source must be within the repository tree." msgstr "" msgid "The destination must be within the repository tree." msgstr "" msgid "Destination file already exists." msgstr "" msgid "Are you sure you want to overwrite it ?" msgstr "" #, python-format msgid "Copy - %s" msgstr "" #, python-format msgid "Rename - %s" 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 "(unsaved)" 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 "" msgctxt "column header" msgid "Filename" msgstr "" msgid "Searching..." msgstr "" #, python-format msgid "filling (%d)" msgstr "" msgid "Mercurial User" 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 "" msgid "&No" msgstr "" 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 "" #, python-format msgid "Unsupported repository type (%s)" msgstr "" msgid "Cannot open non Mercurial repositories or subrepositories" msgstr "" msgid "New Group" msgstr "" msgid "Confirm Delete" msgstr "" #, python-format msgid "Delete Group '%s' and all its entries?" 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 "Close tab" msgstr "" msgid "Close other tabs" msgstr "" msgid "Undo close tab" msgstr "" msgid "Reopen last closed tab" msgstr "" msgid "Undo close other tabs" msgstr "" msgid "Reopen last closed tab group" msgstr "" msgid "Failed to open repository" 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 "C&hoose Log Columns..." msgstr "" msgid "&Resize Columns" msgstr "" #, python-format msgid "Goto ancestor of %s and %s" msgstr "" #, python-format msgid "Can't find revision '%s'" msgstr "" msgid "Drag to change order" msgstr "" msgid "Workbench Log Columns" msgstr "" msgctxt "tab tooltip" msgid "Revision details" msgstr "" msgctxt "tab tooltip" msgid "Commit" msgstr "" msgctxt "tab tooltip" msgid "Search" msgstr "" msgctxt "tab tooltip" msgid "Console log" msgstr "" msgctxt "tab tooltip" msgid "Synchronize" msgstr "" msgctxt "tab tooltip" msgid "Patch Branch" msgstr "" #, python-format msgid "%s " msgstr "" #, python-format msgid "Found %d incoming changesets" msgstr "" msgid "Pull" msgstr "" msgid "Pull incoming changesets into your repository" 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 "" msgid "No revision found" 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 "" #, 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 "Tab cannot exit" 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 "&Merge with Local..." msgstr "" msgid "&Tag..." msgstr "" msgid "Boo&kmark..." msgstr "" msgid "Sig&n..." msgstr "" msgid "&Backout..." msgstr "" msgid "Revert &All Files..." 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 "Change &Phase to" msgstr "" msgid "&Graft to Local..." msgstr "" msgid "Modi&fy History" msgstr "" msgid "&Unapply Patch" msgstr "" msgid "Import to &MQ" msgstr "" msgid "MQ &Options" msgstr "" msgid "&Rebase..." msgstr "" msgid "&Prune..." 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 "Archive DAG Range..." 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 "Graft Selected to local..." msgstr "" msgid "&Prune Selected..." 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 "" "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 "Filter b&y" msgstr "" msgid "&Ancestors and Descendants" msgstr "" msgid "A&uthor" msgstr "" msgid "&Branch" msgstr "" msgid "&More Options..." msgstr "" msgid "Unable to merge" msgstr "" msgid "You cannot merge a revision with itself" msgstr "" msgid "Unable to backout" 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 "" 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 "" msgid "&Edit Toolbar" msgstr "" msgid "&Refresh" msgstr "" msgid "&Filter Toolbar" msgstr "" #, python-format msgid "TortoiseHg: %s" msgstr "" msgid "S&tatus Bar" 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 "Ma&nifest Mode" msgstr "" msgid "Show all version-controlled files in tree view" msgstr "" msgid "&Flat List" msgstr "" msgid "### filter text ###" msgstr "" msgid "Changed by &This Commit" msgstr "" msgid "Show files changed by this commit" msgstr "" msgid "Compare to &1st Parent" msgstr "" msgid "Compare to &2nd Parent" msgstr "" msgid "Toggle parent to be used as the base revision" msgstr "" msgid "List Optio&ns" 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 "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 "" "\n" "Caught keyboard interrupt, aborting.\n" msgstr "" #, python-format msgid "failed to fork GUI process: %s\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 "" #, python-format msgid "abort: %s\n" msgstr "" #, python-format msgid "(%s)\n" msgstr "" msgid "option --config may not be abbreviated!" 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 "set/override config option (use 'section.name=value')" msgstr "" msgid "enable debugging output" 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 "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 debugblockmatcher" msgstr "" msgid "thg debugbugreport [TEXT]" msgstr "" msgid "thg debugconsole" msgstr "" msgid "thg debuglighthg" msgstr "" msgid "thg debugruncommand -- COMMAND [ARGUMENT]..." msgstr "" msgid "no command specified" 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 "select the specified revision" msgstr "" msgid "side-by-side comparison of revisions" msgstr "" msgid "thg filelog [OPTION]... FILE" msgstr "" msgid "requires a single filename" 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 "thg lock" msgstr "" msgid "search for a given text or revset" msgstr "" msgid "(DEPRECATED)" msgstr "" msgid "open a new workbench window" msgstr "" msgid "thg log [OPTIONS] [FILE]" msgstr "" msgid "cannot specify both -k/--query and filenames" 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 "Merge revision not specified or not found" msgstr "" msgid "a revision to post" msgstr "" msgid "thg postreview [-r] REV..." msgstr "" msgid "reviewboard extension not enabled" msgstr "" #, python-format msgid "see %(url)s" msgstr "" msgid "no revisions specified" msgstr "" msgid "revisions to prune" msgstr "" msgid "thg prune [-r] REV..." 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 "do not modify working copy during strip" msgstr "" msgid "revision to strip" msgstr "" msgid "thg strip [-k] [-f] [-n] [[-r] REV]" msgstr "" msgid "open the bookmark sync window" msgstr "" msgid "thg sync [OPTION]... [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 "update 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 "Update to:" msgstr "" msgid "Options:" 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 "remove working directory" msgstr "" msgid "unknown revision!" msgstr "" #, python-format msgid "Remote Update - %s" msgstr "" msgid "&Update" 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 "CLI Editor" msgstr "" msgid "" "The editor used by Mercurial command line commands to collect multiline " "input from the user. Most notably, commit messages." 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. Similarly, %(root)s will be the " "full path to the repository. (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: " "localonly" 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. This setting is ignored when TortoiseHg runs as an OS X " "application bundle. 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: True." msgstr "" msgid "New Repo Skeleton" msgstr "" msgid "" "If specified, files in the directory, e.g. .hgignore, are copied to the " "newly-created repository." msgstr "" msgid "Workbench" 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. 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 sync grep and " "pbranch.
    Default: log commit grep pbranch | sync" msgstr "" msgid "Long Summary" msgstr "" msgid "" "If true, concatenate multiple lines of changeset summary and truncate them " "at 80 characters as necessary. 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.

    • 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" msgstr "" msgid "Show Family Line" msgstr "" msgid "" "Show indirect revision dependency on the revision graph when filtered by " "revset. Default: True

    Note: Calculating family line may be slow in " "some cases. This option is expected to be removed if the performance issue " "is solved." msgstr "" msgid "Use optimized graph layouter" msgstr "" msgid "" "Use alternative graph layouter for large repositories. Default: " "False

    Note: This layouter colors edges using branch information and " "does not display graft edges, regardless of whether they are requested or " "not." 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 "Ask Username" msgstr "" msgid "" "If no username has been specified, the user will be prompted to enter a " "username. Default: False" 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 "Check Subrepo Phase" msgstr "" msgid "" "Check the phase of the current revision of each subrepository. For settings " "other than \"ignore\", the phase of the current revision of each " "subrepository is checked before committing the parent repository. Default: " "follow" 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 shown. 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 shown. 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.

    • 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" 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." "

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

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

    Default: \"ssh\" or \"TortoisePlink." "exe -ssh -2\" (Windows)" msgstr "" msgid "Subrepository Features:" msgstr "" msgid "Allow Hg Subrepos" msgstr "" msgid "" "Whether Mercurial subrepositories are allowed in the working directory. " "Default: True" msgstr "" msgid "Allow Git Subrepos" msgstr "" #, python-format msgid "" "Whether Git subrepositories are allowed in the working directory. Default: " "False

    See the security note before enabling Git " "subrepos." msgstr "" msgid "Allow SVN Subrepos" msgstr "" #, python-format msgid "" "Whether Subversion subrepositories are allowed in the working directory. " "Default: False

    See the security note before enabling " "Subversion subrepos." msgstr "" msgid "Server" msgstr "" msgid "Repository Details:" msgstr "" msgid "" "Repository name to use in the web interface, and by TortoiseHg as a " "shorthand name. Default is the working directory." msgstr "" msgid "Encoding" msgstr "" msgid "" "Character encoding of files in the repository, used by the web interface and " "TortoiseHg." 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 "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 "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 "Method to enable TLS when connecting to mail server. Default: none" 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 "" #, python-brace-format 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} " "tokens are 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:

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

    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:

    • {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}" 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 "Simplelock" msgstr "" msgid "Lock Clone" msgstr "" msgid "" "Location of local clone of organizational lock repository.

    This repository " "must contain a \"locked\" text file" 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: