hgview-1.9.0/0000775000015700001640000000000012607506603013607 5ustar narvalnarval00000000000000hgview-1.9.0/COPYING0000644000015700001640000004311712607505500014641 0ustar narvalnarval00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin St, 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 Library 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, see . Foundation, Inc., 51 Franklin St, 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 Library General Public License instead of this License. hgview-1.9.0/hgext/0000775000015700001640000000000012607506603014726 5ustar narvalnarval00000000000000hgview-1.9.0/hgext/hgview.rc0000644000015700001640000000040312607505500016533 0ustar narvalnarval00000000000000# This file is intended to be installed in a system's hgrc.d directory # It enables the hgview's mercurial plugin, making the 'hg qv' command # available. This file presumes hgext/hgview.py has been installed in # the hgext path [extensions] hgext.hgview = hgview-1.9.0/hgext/hgview.py0000644000015700001640000000677712607505500016603 0ustar narvalnarval00000000000000# hgview: visual mercurial graphlog browser in PyQt4 # # Copyright 2008-2010 Logilab # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. '''browse the repository in a(n other) graphical way The hgview extension allows browsing the history of a repository in a graphical way. It requires PyQt4 with QScintilla. ''' testedwith = '2.9 3.0 3.0.1' buglink = 'https://www.logilab.org/project/hgview' import os import os.path as osp import stat import imp from optparse import Values from mercurial import error, demandimport execpath = osp.abspath(__file__) # resolve symbolic links statinfo = os.lstat(execpath) if stat.S_ISLNK(statinfo.st_mode): execpath = join(osp.dirname(execpath), os.readlink(execpath)) execpath = osp.abspath(execpath) # if standalone, import manually setuppath = osp.join(osp.dirname(osp.dirname(execpath)), 'setup.py') if osp.exists(setuppath): # standalone if setup.py found in src dir hgviewlibpath = osp.join(osp.dirname(osp.dirname(execpath)), 'hgviewlib') hgviewlibpath = osp.abspath(hgviewlibpath) imp.load_package('hgviewlib', hgviewlibpath) # every command must take a ui and and repo as arguments. # opts is a dict where you can find other command line flags # # Other parameters are taken in order from items on the command line that # don't start with a dash. If no default value is given in the parameter list, # they are required. def start_hgview(ui, repo, *pats, **opts): # WARNING, this docstring is superseeded programmatically """ start hgview log viewer ======================= This command will launch the hgview log navigator, allowing to visually browse in the hg graph log, search in logs, and display diff between arbitrary revisions of a file. If a filename is given, launch the filelog diff viewer for this file, and with the '-n' option, launch the filelog navigator for the file. With the '-r' option, launch the manifest viewer for the given revision. """ ### 2.5 compat # We ensure here that we work on unfiltered repo in all case. Unfiltered # repo are repo has we know them now. repo = getattr(repo, 'unfiltered', lambda: repo)() # If this user has a username validation hook enabled, # it could conflict with hgview because both will try to # allocate a QApplication, and PyQt doesn't deal well # with two app instances running under the same context. # To prevent this, we run the hook early before hgview # allocates the app try: from hgconf.uname import hook hook(ui, repo) except ImportError: pass from hgviewlib.application import start def fnerror(text): """process errors""" raise(error.Abort(text)) options = Values(opts) start(repo, options, pats, fnerror) import hgviewlib # note: ``import hgviewlib.hgviewhelp`` is incompatible with standalone # because of the lazy import from hgviewlib.hgviewhelp import long_help_msg start_hgview.__doc__ = long_help_msg cmdtable = { "^hgview|hgv|qv": (start_hgview, [('n', 'navigate', False, '(with filename) start in navigation mode'), ('r', 'rev', '', 'start in manifest navigation mode at rev R'), ('s', 'start', '', 'show only graph from rev S'), ('I', 'interface', '', 'GUI interface to use (among "qt", "raw" and "curses"') ], "hg hgview [options] [filename]"), } hgview-1.9.0/README0000644000015700001640000000342312607505500014462 0ustar narvalnarval00000000000000Description =========== Its purpose is to easily navigate in a Mercurial repository history. It has been written with efficiency in mind, both in terms of computational efficiency and user experience efficiency. It is written in Python. There are two user interfaces: * a graphical intarfece using PyQt4 and QScintilla, the * a text interface: using urwid, pygments and pyinotify Note that the Qt4 interface is much more complete than the text interface. The Qt4 interface provides more views on the repository. hgview intallation notes ======================== hgview can be used either as a hg extension, or as a standalone application. The Common library depends on: mercurial (1.0 minimum) The Qt4 interface depends on PyQt4, QScintilla and PyQScintilla, DocUtils The Text interfaces depend on urwid (>=0.9.1 for "raw", >=1.0.0 for "curses"), pygments and pyinotify Run from the hg repository -------------------------- You can run ``hgview`` without installing it. :: hg clone http://hg.logilab.org/hgview You may want to add the following to your main .hgrc file:: [extensions] hgext.hgview=path/to/hqgv/hgext/hgview.py [hgview] # your hgview configs statements like: dotradius=6 interface=qt # or curses or raw # type hg qv-config to list available options Then from any Mercurial repository:: cd hg qv or:: export PYTHONPATH=PATH_TO_HGVIEW_DIR:$PYTHONPATH PATH_TO_HGVIEW_DIR/bin/hgview Installing ``hgview`` --------------------- Installing ``hgview`` is simply done using usual ``distutils`` script:: cd $PATH_TO_HGVIEW_DIR python setup.py install --help # for available options python setup.py install More informations ================= See `hg help hgview` for more informations on available configuration options. alain hgview-1.9.0/ChangeLog0000644000015700001640000001333612607505500015360 0ustar narvalnarval000000000000002013-01-28 -- 1.7.1 * remove yellow from possible text and line color * compatinility with Mercurial 2.5 * bugfix> prevent obsolescence cycle to confuse hgview * bugfix> fix successors changesets computation * bugfix> fix an issue leading hg to get slower and slower with reload 2012-11-20 -- 1.7.0 * allow copying changeset to clipboard * add support for revset in goto toolbar * standalone executable for hgview on windows * compatibility with Mercurial 2.4 * Use an "open file" dialog when `hgview` is run outside a repo * bugfix> fix badly reported successors 2011-08-08 -- 1.6.2 * generate hgqv.qrc to hgqv_rc.py not hgqv_qrc.py (#103538) 2011-08-08 -- 1.6.1 * Include missing hgviewlib/qt4/application.py (#103444) 2011-08-08 -- 1.6.0 * compat> improve compatibility with Mercurial 2.2 and 2.3 * compat> improve compatibility with Python 2.5 * graph> support for changesets obsolescence * graph> support for hiding closed branches * graph> improve support for hidden changeset * graph> select current working directory parent at startup instead of tip (#82231) * graph> allow to reorder mutable changeset on top (#92312) * graph> add some basic support for bookmarks (#92295, #92750) * config> allow overriding ``maxfilesize`` config value in the UIs (#20597) * config> allow to hide changeset content at startup (#92204) * config> allow per-interface configuration * GUI> allow to mimic the raw text layout by adding "description" to the files list (#83294) * GUI> move "start" and "follow" actions from toolbar to context menu (#87901) * GUI> move diff actions from toolbar to context menu and hide toolbar diff at startup (#87901) * GUI> add icon to "show/hide" hidden changesets action (#87901) * GUI> allow to pass node/rev/tag as link anchor (#87902) * TUI> phases support (#87899) * TUI> use short hash * TUI> goto command now accept any changeid, not only the rev (#92736) * bugfix> Do not reload data from locked repository (#92297) * bugfix> fix encoding error while browsing the content of an unapplied mqpatch that contains unicode chars (#87210) * bugfix> fix OSError while focusing on removed file on mq patches (#87839) * bugfix> fix ``dev/null`` file entry appearing with removed files on unapplied mq patches (#89335) * bugfix> Raw-UI prevent over refreshing with mercurial 2.1 (#89336) * bugfix> do not swallow exception with ``--traceback`` (#89337) * bugfix> Fix some unicode issue in raw text ui (#98647) * bugfix> Properly keep currently visited rev and file on reload (#93641) * bugfix> re-enable copy action in right click menu of the description (#93421) * bugfix> do nothing if the entry of ``find`` is empty in the GUI (#93422) * bugfix> Fix a typo leading to a name error when using bfile. 2011-12-23 -- 1.5.0 * GUI> replace text in description for fancy display (#84465) * GUI> links in fancy view opens browser (#76254) * GUI> Add support for incoming phase feature in hg 2.1 (#86349) Node have different shape given their phase. * TUI> fancier graph highlighting in TUI (#79263) * TUI> add history and completion for command (#84733) * TUI> display text translation in source/diff pane (closes #83773) * Hg> Allow toggle hidden changesets visibility (#19875) * Hg> improve mq support (#19194) * Hg> enable --profile/--time/--traceback/--debug options as mercurial extension (#83309) * support mercurial 2.0 (#79058, #84549) * bugfixes (#77984, #79255, #84939, #78004, #83307) * others (#75296, #78002, #75295) 2011-09-29 -- 1.4.0 * add a text user interface * allow file name to be selected (#70307) * remove mx.Datetime dependency (#73687) * bugfixes (#20996, #73678) 2010-08-25 -- 1.3.0 * ReST support in descriptions * stabilize named branch color * bugfixes 2010-01-20 -- 1.2.0 * add basic support for mq * add basic support for bfiles extension * make working directory be represented by a node in the graph (if needed) * add possibility to display the graph from a given revision * add a annotate-like view * improve graph rendering engine * bugfixes 2009-09-30 -- 1.1.2 * fixed packaging issues 2009-09-23 -- 1.1.0 * add many configuration options * removed 'hg hgview-options' command in favor of 'hg help hgview' * add ability to choose which parent to diff with for merge nodes * dramatically improved UI behaviour (shortcuts) * improved help * make it possible not to display the diffstat column * standalone application: improved command line options * indicate working directory position in the graph * add auto-reload feature (when repo is modified; pull, commit, etc.) * fix many bugs 2009-06-08 -- 1.0.1 * fix a bug on some PyQt versions, which also prevented hardy package from installing 2009-06-05 -- 1.0.0 * almost complete rewrite of hgview (only using Qt for now) * make dialogs consistants * pure qt4 * add a file revision comparator * add a manifest viewer 2008-10-06 -- 0.9.0 * support branches * make filter text search on files and log description tree. * bugfixes 2008-05-15 -- 0.3.1 * added logic to resolve symbolic links to hgview executable * make hgview less verbose * allow home installation * let the diff's scroll window resize when it's packed in the HPaned * fix Windows related bug 2007-05-29 -- 0.3.0 * add a Qt4 version of hgview * bugfixes 2007-05-29 -- 0.2.0 * creation of changelog hgview-1.9.0/doc/0000775000015700001640000000000012607506603014354 5ustar narvalnarval00000000000000hgview-1.9.0/doc/Makefile0000644000015700001640000000115012607505500016002 0ustar narvalnarval00000000000000SOURCES=hgview.1.txt #$(wildcard *.[0-9].txt) MAN=$(SOURCES:%.txt=%) HTML=$(SOURCES:%.txt=%.html) PREFIX=/usr/local MANDIR=$(PREFIX)/man INSTALL=install -c all: man html man: $(MAN) html: $(HTML) %: %.xml xmlto man $*.xml %.xml: %.txt asciidoc -d manpage -b docbook -o $@ $< %.html: %.txt asciidoc -b html4 -o $@ $< || asciidoc -b html -o $@ $< install: man for i in $(MAN) ; do \ subdir=`echo $$i | sed -n 's/..*\.\([0-9]\)$$/man\1/p'` ; \ $(INSTALL) -d $(DESTDIR)$(MANDIR)/$$subdir && \ $(INSTALL) $$i $(DESTDIR)$(MANDIR)/$$subdir ; \ done clean: $(RM) $(MAN) $(MAN:%=%.xml) $(MAN:%=%.html) hgview-1.9.0/doc/hgview.1.txt0000644000015700001640000000447712607505500016552 0ustar narvalnarval00000000000000hgview(1) ========= David Douard NAME ---- hgview - Qt based mercurial repository browser SYNOPSIS -------- 'hgview' [options] [filename] DESCRIPTION ----------- hgview(1) is a GUI application usually invoked from the command line. The simplest way to use it is to install it as a hg extension. Alternatively, it can be used as a standalone application. If [filename] is given, hgview will start in file-diff mode, in which user can easily compare arbitrary revisions of a file. Use ``hg help hgview`` for an extended help description OPTIONS ------- `-n`, --navigate (require a filename):: starts in filelog navigation mode `-r REV`, --rev=REV:: starts in manifest mode for given revision FILES ----- ~/.hgrc:: This is the standard file for configuring hg and its extensions. See `hg qv-config` for more details on what can be configured this way. ~/.hgusers:: This file holds configurations related to authors of patches in the hg repository. See `hg qv-config` for more details on what can be configured this way. BUGS ---- Please report any found bug on the mailing list or via email. Patches (or mercurial bundles) are always welcome. AUTHOR ------ Current version has been mainly written by David Douard , based on hgview 0.x code which has been written by Ludovic Aubry, Graziella Toutoungis and others. RESOURCES --------- http://www.logilab.org/project/hgview COPYRIGHT --------- Copyright \(C) 2012 David Douard (david.douard@logilab.fr). Copyright \(C) 2007-2012 LOGILAB S.A. (Paris, FRANCE), http://www.logilab.fr/ -- mailto:contact@logilab.fr LICENSING --------- 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 pro‐ gram; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. hgview-1.9.0/__pkginfo__.py0000777000015700001640000000000012607505500023163 2hgviewlib/__pkginfo__.pyustar narvalnarval00000000000000hgview-1.9.0/PKG-INFO0000664000015700001640000000515412607506603014711 0ustar narvalnarval00000000000000Metadata-Version: 1.0 Name: hgview Version: 1.9.0 Summary: a Mercurial interactive history viewer Home-page: http://www.logilab.org/projects/hgview Author: Logilab Author-email: python-projects@lists.logilab.org License: GPL Description: Description =========== Its purpose is to easily navigate in a Mercurial repository history. It has been written with efficiency in mind, both in terms of computational efficiency and user experience efficiency. It is written in Python. There are two user interfaces: * a graphical intarfece using PyQt4 and QScintilla, the * a text interface: using urwid, pygments and pyinotify Note that the Qt4 interface is much more complete than the text interface. The Qt4 interface provides more views on the repository. hgview intallation notes ======================== hgview can be used either as a hg extension, or as a standalone application. The Common library depends on: mercurial (1.0 minimum) The Qt4 interface depends on PyQt4, QScintilla and PyQScintilla, DocUtils The Text interfaces depend on urwid (>=0.9.1 for "raw", >=1.0.0 for "curses"), pygments and pyinotify Run from the hg repository -------------------------- You can run ``hgview`` without installing it. :: hg clone http://hg.logilab.org/hgview You may want to add the following to your main .hgrc file:: [extensions] hgext.hgview=path/to/hqgv/hgext/hgview.py [hgview] # your hgview configs statements like: dotradius=6 interface=qt # or curses or raw # type hg qv-config to list available options Then from any Mercurial repository:: cd hg qv or:: export PYTHONPATH=PATH_TO_HGVIEW_DIR:$PYTHONPATH PATH_TO_HGVIEW_DIR/bin/hgview Installing ``hgview`` --------------------- Installing ``hgview`` is simply done using usual ``distutils`` script:: cd $PATH_TO_HGVIEW_DIR python setup.py install --help # for available options python setup.py install More informations ================= See `hg help hgview` for more informations on available configuration options. alain Platform: UNKNOWN hgview-1.9.0/hgviewlib/0000775000015700001640000000000012607506603015567 5ustar narvalnarval00000000000000hgview-1.9.0/hgviewlib/qt4/0000775000015700001640000000000012607506603016277 5ustar narvalnarval00000000000000hgview-1.9.0/hgviewlib/qt4/styleditemdelegate.py0000644000015700001640000002126712607505500022530 0ustar narvalnarval00000000000000# Copyright (c) 2009-2013 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, see . """ Contains the StyledItemDelegate used as item deleate by qt to render log table cell. """ from functools import wraps from PyQt4.QtCore import QSize, Qt, QPointF from PyQt4.QtGui import QStyledItemDelegate, QStyleOptionViewItemV4, \ QStyle, QPixmap, QColor, QPen, QPainter, QTextDocument, \ QAbstractTextDocumentLayout, QPalette from hgviewlib.hgpatches import phases from hgviewlib.qt4 import icon as geticon def secured(func): """A Decorator that call ``func(self, painter, *ags, **kws)`` in a secured way, e.g. the painter is returned to the state it was supplied in when ``func`` was called. """ @wraps(func) def _func(self, painter, *ags, **kws): painter.save() try: out = func(self, painter, *ags, **kws) finally: painter.restore() return out return _func class StyledItemDelegate(QStyledItemDelegate): """Render styled column content.""" def __init__(self, parent=0): super(StyledItemDelegate, self).__init__(parent) self._model = None def paint(self, painter, option, index): """Render the Delegate using the given ``painter`` and style ``option`` for the item specified by ``index``. """ self._model = index.model() # draw selection option = QStyleOptionViewItemV4(option) self.parent().style().drawControl(QStyle.CE_ItemViewItem, option, painter) self._draw_content(painter, option, index) def _draw_content(self, painter, option, index): self._draw_background(painter, option, index) self._draw_text(painter, option, index) @secured def _draw_background(self, painter, option, index): """Draw the background if specified by the model excepts when the row is selected in which case the selection color has precedence. """ background = self._model.data(index, Qt.BackgroundRole) # we draw specified background except when row is selected as selection color # always have precedence if background is not None and not option.state & QStyle.State_Selected: painter.fillRect(option.rect, background) @secured def _draw_text(self, painter, option, index): """Draw the content text in the cell. We currently use the default styled item delegate to render the cell content. """ doc = QTextDocument() text = self._model.data(index, Qt.DisplayRole) doc.setHtml(text) painter.setClipRect(option.rect) painter.translate(QPointF( option.rect.left(), option.rect.top() + (option.rect.height() - doc.size().height()) / 2)) layout = QAbstractTextDocumentLayout.PaintContext() layout.palette = option.palette if option.state & QStyle.State_Selected: if option.state & QStyle.State_Active: layout.palette.setCurrentColorGroup(QPalette.Active) else: layout.palette.setCurrentColorGroup(QPalette.Inactive) layout.palette.setBrush(QPalette.Text, layout.palette.highlightedText()) elif not option.state & QStyle.State_Enabled: layout.palette.setCurrentColorGroup(QPalette.Disabled) doc.documentLayout().draw(painter, layout) class GraphItemDelegate(StyledItemDelegate): """Render revisions tree graph and styled column content.""" def _graph_width(self, nb_branches): """Return graph width in pix for ``nb_branches``. """ return (self._model.dot_radius + 2) * nb_branches + 2 # max pen width def _draw_content(self, painter, option, index): self._draw_background(painter, option, index) self._draw_graph(painter, option, index) self._draw_text(painter, option, index) @secured def _draw_graph(self, painter, option, index): """ Draw the tree edges for the mercurial context ``ctx`` at the graph node ``gnode``. ..note:: ``ctx`` and ``gnode`` is quite redundant as ``ctx=self.repo.changectx(gnode.rev)``. But this avoids computing ``ctx`` twice. """ painter.setClipRect(option.rect) painter.translate(QPointF(option.rect.left(), option.rect.top())) row = index.row() gnode = index.model().graph[row] ctx = index.model().repo.changectx(gnode.rev) w = self._graph_width(gnode.cols) h = option.rect.height() pix = QPixmap(w, h) pix.fill(QColor(0,0,0,0)) self._draw_graph_ctx(painter, pix, ctx, gnode) option.rect.setLeft(option.rect.left() + w + 5) def _draw_graph_ctx(self, painter, pix, ctx, gnode): h = pix.height() radius = self._model.dot_radius dot_x = self._graph_width(gnode.x) dot_y = h / 2 painter.setRenderHint(QPainter.Antialiasing) for y1, y2, lines in ((dot_y, dot_y + h, gnode.bottomlines), (dot_y - h, dot_y, gnode.toplines)): for start, end, color, fill in lines: x1 = self._graph_width(start) + radius / 2 x2 = self._graph_width(end) + radius / 2 color = QColor(self._model.get_color(color)) _draw_graph_line(painter, x1, x2, y1, y2, color, not fill) dot_color = QColor(self._model.namedbranch_color(ctx.branch())) self._draw_graph_node(painter, dot_x, dot_y, radius, dot_color, ctx) def _draw_graph_node(self, painter, x, y, r, color, ctx): y -= r / 2 # middle -> border tags = set(ctx.tags()) phase = ctx.phase() if ctx.rev() is None: # WD is displayed only if there are local # modifications, so let's use the modified icon _draw_graph_node_modified(painter, x, y) elif tags.intersection(self._model.mqueues): _draw_graph_node_mqpatch(painter, x, y) elif phase == phases.draft: self._draw_graph_node_draft(painter, x, y, r, color, ctx) elif phase == phases.secret: self._draw_graph_node_secret(painter, x, y, r, color, ctx) else: self._draw_graph_node_public(painter, x, y, r, color, ctx) def _set_graph_node_style(self, painter, dot_color, ctx): rev = ctx.rev() dotcolor = QColor(dot_color) if ctx.obsolete(): penradius = 1 pencolor = QColor(dotcolor) pencolor.setAlpha(150) elif rev in self._model.heads: penradius = 2 pencolor = dotcolor.darker() else: penradius = 1 pencolor = Qt.black if rev in self._model.wd_revs: pen = QPen(Qt.red) pen.setWidth(2) else: pen = QPen(pencolor) pen.setWidth(1) painter.setPen(pen) painter.setBrush(dotcolor) def _draw_graph_node_public(self, painter, x, y, r, color, ctx): if ctx.rev() in self._model.wd_revs: geticon('clean').paint(painter, x - 5, y - 5, 17, 17) else: self._set_graph_node_style(painter, color, ctx) painter.drawEllipse(x, y, r, r) def _draw_graph_node_draft(self, painter, x, y, r, color, ctx): self._set_graph_node_style(painter, color, ctx) painter.drawRect(x, y, r, r) def _draw_graph_node_secret(self, painter, x, y, r, color, ctx): self._set_graph_node_style(painter, color, ctx) painter.drawPolygon( QPointF(x + (r // 2), y), QPointF(x, y + r), QPointF(x + r, y + r) ) def _draw_graph_node_mqpatch(painter, x, y): geticon('mqpatch').paint(painter, x - 5, y - 5, 17, 17) def _draw_graph_node_modified(painter, x, y): geticon('modified').paint(painter, x - 5, y - 5, 17, 17) def _draw_graph_line(painter, x1, x2, y1, y2, color, isobsolete): lpen = QPen(color) if isobsolete: lpen.setStyle(Qt.DotLine) color.setAlpha(150) lpen.setWidth(2) painter.setPen(lpen) painter.drawLine(x1, y1, x2, y2) hgview-1.9.0/hgviewlib/qt4/helpviewer.py0000644000015700001640000000630312607505500021016 0ustar narvalnarval00000000000000# -*- coding: iso-8859-1 -*- # main.py - qt4-based hg rev log browser # # 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. """ Help window for hgview """ import sys, os import re from PyQt4 import QtCore, QtGui, Qsci from hgviewlib.qt4 import icon as geticon from hgviewlib.qt4.mixins import HgDialogMixin, ui2cls from hgviewlib.hgviewhelp import help_msg, get_options_helpmsg Qt = QtCore.Qt bold = QtGui.QFont.Bold try: from docutils.core import publish_string except: def publish_string(s, *args, **kwargs): return s class HelpViewer(HgDialogMixin, ui2cls('helpviewer.ui'), QtGui.QDialog): """hgview simple help viewer""" def __init__(self, repo, rest, parent=None): self.repo = repo super(HelpViewer, self).__init__(parent) self.load_ui() self.load_config(repo) self.set_text(rest) self.textBrowser.anchorClicked.connect(self.anchor_clicked) def set_text(self, rest): formated_text = publish_string(rest, writer_name='html') self.textBrowser.setText(formated_text) def anchor_clicked(self, qurl): """Callback called when a link is clicked in the text browser""" QtGui.QDesktopServices.openUrl(qurl) # must be redefined cause it's a QDialog def accept(self): QtGui.QDialog.accept(self) def reject(self): QtGui.QDialog.reject(self) class HgHelpViewer(HelpViewer): """Display a Mercurial help topic and allow to navigate in hg topics""" def __init__(self, repo, topic, parent=None): super(HgHelpViewer, self).__init__(repo, self.get_hg_helper(topic), parent) def set_text(self, rest): # handle cross hg doc references with special url for reference in re.findall(':hg:`.*?`', rest): topic = reference[5:-1] if not self.get_hg_helper(topic): rest = re.sub(reference, '*%s*' % topic, rest) else: rest = re.sub(reference, '`%s `_'%(topic,topic), rest) formated_text = publish_string(rest, writer_name='html') self.textBrowser.setText(formated_text) def anchor_clicked(self,qurl): """Callback called when a link is clicked in the text browser""" topic = qurl.toString() if topic.startswith('hg://'): self.set_text(self.get_hg_helper(topic[5:])) else: QtGui.QDesktopServices.openUrl(qurl) @staticmethod def get_hg_helper(topic): from mercurial import help, cmdutil, commands, error try: rest = cmdutil.findcmd(topic, commands.table, strict=False)[1][0]() except (error.UnknownCommand, error.SignatureError, error.AmbiguousCommand, KeyError): try: rest = (f for n,h,f in help.helptable if topic in n).next()() except StopIteration: rest = None return rest class HgviewHelpViewer(HelpViewer): def __init__(self, repo, parent=None): rest = help_msg + get_options_helpmsg(rest=True) super(HgviewHelpViewer, self).__init__(repo, rest, parent) hgview-1.9.0/hgviewlib/qt4/hgfiledialog.py0000644000015700001640000004416712607505500021274 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ Qt4 dialogs to display hg revisions of a file """ import sys, os import os.path as osp import difflib from mercurial import ui, hg, util from PyQt4 import QtGui, QtCore from PyQt4.QtCore import Qt, pyqtSignal from hgviewlib.application import (FileViewer as _FileViewer, FileDiffViewer as _FileDiffViewer) from hgviewlib.util import tounicode, rootpath from hgviewlib.qt4 import icon as geticon from hgviewlib.qt4.mixins import HgDialogMixin, ActionsMixin, ui2cls from hgviewlib.qt4.hgrepomodel import FileRevModel from hgviewlib.qt4.blockmatcher import BlockList, BlockMatch from hgviewlib.qt4.quickbar import FindInGraphlogQuickBar from hgviewlib.qt4.widgets import SourceViewer sides = ('left', 'right') otherside = {'left': 'right', 'right': 'left'} class AbstractFileDialog(ActionsMixin, HgDialogMixin, QtGui.QMainWindow): def __init__(self, repo, filename, repoviewer=None): self.repo = repo super(AbstractFileDialog, self).__init__() self.load_ui() self.load_config(self.repo) self.setRepoViewer(repoviewer) self._show_rev = None self.filename = filename self.createActions() self.setupToolbars() self.setupViews() self.setupModels() def setRepoViewer(self, repoviewer=None): self.repoviewer = repoviewer if repoviewer: repoviewer.destroyed.connect(lambda x: self.setRepoViewer()) def reload(self): self.repo = util.build_repo(self.repo.ui, self.repo.root) self.setupModels() def modelFilled(self): self.filerevmodel.filled.disconnect(self.modelFilled) if isinstance(self._show_rev, int): index = self.filerevmodel.indexFromRev(self._show_rev) self._show_rev = None else: index = self.filerevmodel.index(0,0) self.tableView_revisions.setCurrentIndex(index) def revisionActivated(self, rev): """ Callback called when a revision is double-clicked in the revisions table """ if self.repoviewer is None: # prevent recursive import from hgviewlib.qt4.hgrepoviewer import HgRepoViewer self.repoviewer = HgRepoViewer(self.repo) self.repoviewer.goto(rev) self.repoviewer.show() self.repoviewer.activateWindow() self.repoviewer.raise_() class FileViewer(AbstractFileDialog, ui2cls('fileviewer.ui'), _FileViewer): """ A dialog showing a revision graph for a file. """ def setupViews(self): self.textView.setFont(self._font) self.setWindowTitle('hgview filelog: %s' % os.path.abspath(self.filename)) self.textView.message_logged.connect(self.statusBar().showMessage) def setupToolbars(self): self.find_toolbar = FindInGraphlogQuickBar(self) self.find_toolbar.attachFileView(self.textView) self.find_toolbar.revision_selected.connect( self.tableView_revisions.goto) self.find_toolbar.revision_selected[int].connect( self.tableView_revisions.goto) self.find_toolbar.message_logged.connect(self.statusBar().showMessage) self.attachQuickBar(self.find_toolbar) add_actions = self.toolBar_edit.addActions self.toolBar_edit.addSeparator() add_actions(self.tableView_revisions.get_actions('back', 'forward')) self.toolBar_edit.addSeparator() add_actions(self.get_actions('diffmode', 'annmode', 'next', 'prev')) self.attachQuickBar(self.tableView_revisions.goto_toolbar) def setupModels(self): self.filerevmodel = FileRevModel(self.repo) self.tableView_revisions.setModel(self.filerevmodel) self.tableView_revisions.revision_selected.connect( self.revisionSelected) self.tableView_revisions.revision_selected[int].connect( self.revisionSelected) self.tableView_revisions.revision_activated.connect( self.revisionActivated) self.tableView_revisions.revision_activated[int].connect( self.revisionActivated) self.filerevmodel.message_logged.connect( self.statusBar().showMessage, Qt.QueuedConnection) self.filerevmodel.filled.connect(self.modelFilled) self.textView.setMode('file') self.textView.setModel(self.filerevmodel) self.find_toolbar.setModel(self.filerevmodel) self.find_toolbar.setFilterFiles([self.filename]) self.find_toolbar.setMode('file') self.filerevmodel.setFilename(self.filename) def createActions(self): self.add_action( 'close', self.actionClose, icon='quit', callback=lambda: self.close(), ) self.add_action( 'reload', self.actionReload, icon='reload', callback=lambda: self.reload(), ) self.add_action( 'diffmode', self.tr("Diff mode"), menu=self.tr("Mode"), icon='diffmode' , tip=self.tr("Enable/Disable Diff mode"), callback=self.setMode, checked=False, ) self.add_action( 'annmode', self.tr("Annotate mode"), tip=self.tr("Enable/Disable Annotate mode"), callback=self.textView.setAnnotate, checked=False, ) self.add_action( 'next', self.tr("Next hunk"), menu=self.tr("Moves"), icon='down', tip=self.tr("Jump to the next hunk"), keys=[Qt.ALT + Qt.Key_Down], callback=lambda: self.nextDiff(), ) self.add_action( 'prev', self.tr("Prior hunk"), menu=self.tr("Moves"), icon='up', tip=self.tr("Jump to the previous hunk"), keys=[Qt.ALT + Qt.Key_Up], callback=lambda: self.prevDiff(), ) def revisionSelected(self, rev): pos = self.textView.verticalScrollBar().value() ctx = self.filerevmodel.repo.changectx(rev) self.textView.setContext(ctx) self.textView.displayFile(self.filerevmodel.graph.filename(rev)) self.textView.verticalScrollBar().setValue(pos) self.set_action('prev', enabled=False) self.textView.filled.connect( lambda self=self: self.set_action('next', enabled=self.textView.fileMode() and self.textView.nDiffs())) def goto(self, rev): index = self.filerevmodel.indexFromRev(rev) if index is not None: self.tableView_revisions.setCurrentIndex(index) else: self._show_rev = rev def setMode(self, mode): self.textView.setMode(mode) self.set_actions('annmode', 'next', 'prev', enabled=not mode) def nextDiff(self): notlast = self.textView.nextDiff() enabled = self.textView.fileMode() and notlast and self.textView.nDiffs() self.set_action('next', enabled=(enabled if enabled is not None else True)) enabled = self.textView.fileMode() and self.textView.nDiffs() self.set_action('prev', enable=(enabled if enabled is not None else True)) def prevDiff(self): notfirst = self.textView.prevDiff() self.set_action('prev', enabled=self.textView.fileMode() and notfirst and self.textView.nDiffs()) self.set_action('next', enabled=self.textView.fileMode() and self.textView.nDiffs()) class FileDiffViewer(AbstractFileDialog, ui2cls('filediffviewer.ui'), _FileDiffViewer): """ Qt4 dialog to display diffs between different mercurial revisions of a file. """ diff_filled = pyqtSignal() def setupViews(self): self.tableView_revisions = self.tableView_revisions_left self.tableViews = {'left': self.tableView_revisions_left, 'right': self.tableView_revisions_right} # viewers are Scintilla editors self.viewers = {} # block are diff-block displayers self.block = {} self.diffblock = BlockMatch(self.frame) lay = QtGui.QHBoxLayout(self.frame) lay.setSpacing(0) lay.setContentsMargins(0, 0, 0, 0) for side, idx in (('left', 0), ('right', 3)): textview = SourceViewer(self.frame) textview.setFont(self._font) textview.verticalScrollBar().setFocusPolicy(Qt.StrongFocus) textview.setFocusProxy(textview.verticalScrollBar()) textview.verticalScrollBar().installEventFilter(self) lay.addWidget(textview) self.viewers[side] = textview blk = BlockList(self.frame) blk.linkScrollBar(textview.verticalScrollBar()) self.diffblock.linkScrollBar(textview.verticalScrollBar(), side) lay.insertWidget(idx, blk) self.block[side] = blk lay.insertWidget(2, self.diffblock) for side in sides: table = getattr(self, 'tableView_revisions_%s' % side) table.setTabKeyNavigation(False) #table.installEventFilter(self) table.revision_selected.connect(self.revisionSelected) table.revision_selected[int].connect(self.revisionSelected) table.revision_activated.connect(self.revisionActivated) table.revision_activated[int].connect(self.revisionActivated) self.viewers[side].verticalScrollBar().valueChanged[int].connect( lambda value, side=side: self.vbar_changed(value, side)) self.attachQuickBar(table.goto_toolbar) self.setTabOrder(table, self.viewers['left']) self.setTabOrder(self.viewers['left'], self.viewers['right']) self.setWindowTitle('hgview diff: %s' % os.path.abspath(self.filename)) # timer used to fill viewers with diff block markers during GUI idle time self.timer = QtCore.QTimer() self.timer.setSingleShot(False) self.timer.timeout.connect(self.idle_fill_files) def setupModels(self): self.filedata = {'left': None, 'right': None} self._invbarchanged = False self.filerevmodel = FileRevModel(self.repo, self.filename) self.filerevmodel.filled.connect(self.modelFilled) self.tableView_revisions_left.setModel(self.filerevmodel) self.tableView_revisions_right.setModel(self.filerevmodel) def createActions(self): self.add_action( 'close', self.actionClose, icon='quit', callback=lambda: self.close(), ) self.add_action( 'reload', self.actionReload, icon='reload', callback=lambda: self.reload(), ) self.add_action( 'next', self.tr("Next hunk"), menu=self.tr("Moves"), icon='down', tip=self.tr("Jump to the next hunk"), keys=[Qt.ALT + Qt.Key_Down], callback=lambda: self.nextDiff(), enabled=False, ) self.add_action( 'prev', self.tr("Prior hunk"), menu=self.tr("Moves"), icon='up', tip=self.tr("Jump to the previous hunk"), keys=[Qt.ALT + Qt.Key_Up], callback=lambda: self.prevDiff(), enabled=False, ) def setupToolbars(self): self.toolBar_edit.addSeparator() self.toolBar_edit.addActions(self.get_actions('next', 'prev')) def modelFilled(self): self.filerevmodel.filled.disconnect(self.modelFilled) if self._show_rev is not None: rev = self._show_rev self._show_rev = None else: rev = self.filerevmodel.graph[0].rev self.goto(rev) def revisionSelected(self, rev): if self.sender() is self.tableView_revisions_right: side = 'right' else: side = 'left' path = self.filerevmodel.graph.nodesdict[rev].extra[0] fc = self.repo.changectx(rev).filectx(path) self.filedata[side] = tounicode(fc.data()).splitlines() self.update_diff(keeppos=otherside[side]) def goto(self, rev): index = self.filerevmodel.indexFromRev(rev) if index is not None: if index.row() == 0: index = self.filerevmodel.index(1, 0) self.tableView_revisions_left.setCurrentIndex(index) index = self.filerevmodel.index(0, 0) self.tableView_revisions_right.setCurrentIndex(index) else: self._show_rev = rev def setDiffNavActions(self, pos=0): hasdiff = (self.diffblock.nDiffs() > 0) self.set_action('next', enabled=hasdiff and pos != 1) self.set_action('prev', enabled=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) self.diff_filled.emit() break tag, alo, ahi, blo, bhi = self._diff.get_opcodes().pop(0) 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, w.markertriangle) w = self.viewers['right'] for i in range(blo, bhi): w.markerAdd(i, w.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, w.markerminus) elif tag == 'insert': self.block['right'].addBlock('+', blo, bhi) self.diffblock.addBlock('+', alo, ahi, blo, bhi) w = self.viewers['right'] for i in range(blo, bhi): w.markerAdd(i, w.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].set_text(self.filename, '\n'.join(self.filedata[side]), flag='+', cfg=self.cfg) self.update_page_steps(keeppos) self.timer.start() def vbar_changed(self, value, side): """ Callback called when the vertical scrollbar of a file viewer is changed, so we can update the position of the other file viewer. """ if self._invbarchanged: # 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 hgview-1.9.0/hgviewlib/qt4/filediffviewer.ui0000644000015700001640000001042012607505500021616 0ustar narvalnarval00000000000000 MainWindow 0 0 620 546 hgview diff 0 33 620 513 0 Qt::Vertical 0 true QAbstractItemView::SingleSelection QAbstractItemView::SelectRows false true QAbstractItemView::SingleSelection QAbstractItemView::SelectRows false 0 QFrame::NoFrame QFrame::Raised 0 0 121 33 toolBar TopToolBarArea true 121 0 499 33 toolBar_2 TopToolBarArea false Close Ctrl+Q Reload Ctrl+R RevisionsTableView QTableView
revisions_table.h
tableView_revisions_left tableView_revisions_right
hgview-1.9.0/hgviewlib/qt4/lexers.py0000644000015700001640000001134212607505500020145 0ustar narvalnarval00000000000000import re from PyQt4 import QtCore, QtGui, Qsci, uic from PyQt4.QtCore import Qt class _LexerSelector(object): _lexer = None def match(self, filename, filedata): return False def lexer(self, cfg=None): """ Return a configured instance of the lexer """ return self.cfg_lexer(self._lexer(), cfg) #pylint: disable=E1102 def cfg_lexer(self, lexer, cfg=None): if cfg: font = QtGui.QFont() fontstr = cfg.getFont() font.fromString(fontstr) size = cfg.getFontSize() else: font = QtGui.QFont('Monospace') size = 9 font.setPointSize(size) lexer.setFont(font, -1) 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: 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 CppLexerSelector(_FilenameLexerSelector): extensions = ('.c', '.cpp', '.cxx', '.h', '.hpp', '.hxx') _lexer = Qsci.QsciLexerCPP class CSSLexerSelector(_FilenameLexerSelector): extensions = ('.css',) _lexer = Qsci.QsciLexerCSS class HTMLLexerSelector(_FilenameLexerSelector): extensions = ('.htm', '.html', '.xhtml', '.xml') _lexer = Qsci.QsciLexerHTML class MakeLexerSelector(_FilenameLexerSelector): extensions = ('.mk', 'makefile') _lexer = Qsci.QsciLexerMakefile class SQLLexerSelector(_FilenameLexerSelector): extensions = ('.sql',) _lexer = Qsci.QsciLexerSQL class JSLexerSelector(_FilenameLexerSelector): extensions = ('.js',) _lexer = Qsci.QsciLexerJavaScript class JavaLexerSelector(_FilenameLexerSelector): extensions = ('.java',) _lexer = Qsci.QsciLexerJava class TeXLexerSelector(_FilenameLexerSelector): extensions = ('.tex', '.latex',) _lexer = Qsci.QsciLexerTeX class DiffLexerSelector(_ScriptLexerSelector): extensions = () _lexer = Qsci.QsciLexerDiff regex = re.compile(r'^@@ [-]\d+,\d+ [+]\d+,\d+ @@$') def cfg_lexer(self, lexer, cfg=None): """ Return a configured instance of the lexer """ if cfg: lexer.setDefaultPaper(QtGui.QColor(cfg.getDiffBGColor())) lexer.setColor(QtGui.QColor(cfg.getDiffFGColor()), -1) lexer.setColor(QtGui.QColor(cfg.getDiffPlusColor()), 6) lexer.setColor(QtGui.QColor(cfg.getDiffMinusColor()), 5) lexer.setColor(QtGui.QColor(cfg.getDiffSectionColor()), 4) font = QtGui.QFont() fontstr = cfg.getFont() font.fromString(fontstr) size = cfg.getFontSize() else: font = QtGui.QFont('Monospace') size = 9 font.setPointSize(size) lexer.setFont(font, -1) bfont = QtGui.QFont(font) bfont.setBold(True) lexer.setFont(bfont, 5) lexer.setFont(bfont, 6) return lexer lexers = [cls() for clsname, cls in globals().items() if not clsname.startswith('_') and isinstance(cls, type) and \ issubclass(cls, (_LexerSelector, _FilenameLexerSelector, _ScriptLexerSelector))] def get_lexer(filename, filedata, fileflag=None, cfg=None): if fileflag == "=": return DiffLexerSelector().lexer(cfg) for lselector in lexers: if lselector.match(filename, filedata): return lselector.lexer(cfg) return None hgview-1.9.0/hgviewlib/qt4/manifestviewer.ui0000644000015700001640000000462512607505500021666 0ustar narvalnarval00000000000000 MainWindow 0 0 400 300 hgview manifest 0 33 400 267 2 Qt::Horizontal 1 0 3 0 QFrame::StyledPanel QFrame::Raised 0 0 400 33 toolBar TopToolBarArea false Close Ctrl+Q Reload Ctrl+R hgview-1.9.0/hgviewlib/qt4/blockmatcher.py0000644000015700001640000002533612607505500021311 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ Qt4 widgets to display diffs as blocks """ import sys, os from functools import partial from PyQt4 import QtGui, QtCore from PyQt4.QtCore import Qt, pyqtSignal class BlockList(QtGui.QWidget): """ A simple widget to be 'linked' to the scrollbar of a diff text view. It represents diff blocks with colored rectangles, showing currently viewed area by a semi-transparant rectangle sliding above them. """ value_changed = pyqtSignal(int) range_changed = pyqtSignal(int, int) page_step_changed = pyqtSignal(int) def __init__(self, *args): QtGui.QWidget.__init__(self, *args) self._blocks = set() self._minimum = 0 self._maximum = 100 self.blockTypes = {'+': QtGui.QColor(0xA0, 0xFF, 0xB0, ),#0xa5), '-': QtGui.QColor(0xFF, 0xA0, 0xA0, ),#0xa5), 'x': QtGui.QColor(0xA0, 0xA0, 0xFF, ),#0xa5), } self._sbar = None self._value = 0 self._pagestep = 10 self._vrectcolor = QtGui.QColor(0x00, 0x00, 0x55, 0x25) self._vrectbordercolor = self._vrectcolor.darker() self.sizePolicy().setControlType(QtGui.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.range_changed[int, int].emit(self._minimum, self._maximum) def setMinimum(self, minimum): self._minimum = minimum self.update() self.range_changed[int, int].emit(self._minimum, self._maximum) def setRange(self, minimum, maximum): self._minimum = minimum self._maximum = maximum self.update() self.range_changed[int, int].emit(self._minimum, self._maximum) def setValue(self, val): if val != self._value: self._value = val self.update() self.value_changed[int].emit(val) def setPageStep(self, pagestep): if pagestep != self._pagestep: self._pagestep = pagestep self.update() self.page_step_changed[int].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[int].connect(self.setValue) sbar.rangeChanged[int, int].connect(self.setRange) # use partial to bypass the slot overload checking that fails with # pyqt4 4.10.1 on debian self.value_changed[int].connect(partial(sbar.setValue)) self.range_changed[int, int].connect(partial(sbar.setRange)) self.page_step_changed[int].connect(partial(sbar.setPageStep)) def syncPageStep(self): self.setPageStep(self._sbar.pageStep()) def paintEvent(self, event): w = self.width() - 1 h = self.height() p = QtGui.QPainter(self) p.scale(1.0, float(h)/(self._maximum - self._minimum + self._pagestep)) p.setPen(Qt.NoPen) for typ, alo, ahi in self._blocks: p.save() p.setBrush(self.blockTypes[typ]) p.drawRect(1, alo, w-1, ahi-alo) p.restore() p.save() p.setPen(self._vrectbordercolor) p.setBrush(self._vrectcolor) p.drawRect(0, self._value, w, self._pagestep) p.restore() class BlockMatch(BlockList): """ A simple 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. """ value_changed = pyqtSignal([int], [int, str]) range_changed = pyqtSignal([int, int], [int, int, str]) page_step_changed = pyqtSignal([int], [int, str]) def __init__(self, *args): QtGui.QWidget.__init__(self, *args) self._blocks = set() self._minimum = {'left': 0, 'right': 0} self._maximum = {'left': 100, 'right': 100} self.blockTypes = {'+': QtGui.QColor(0xA0, 0xFF, 0xB0, ),#0xa5), '-': QtGui.QColor(0xFF, 0xA0, 0xA0, ),#0xa5), 'x': QtGui.QColor(0xA0, 0xA0, 0xFF, ),#0xa5), } self._sbar = {} self._value = {'left': 0, 'right': 0} self._pagestep = {'left': 10, 'right': 10} self._vrectcolor = QtGui.QColor(0x00, 0x00, 0x55, 0x25) self._vrectbordercolor = self._vrectcolor.darker() self.sizePolicy().setControlType(QtGui.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): w = self.width() h = self.height() p = QtGui.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 = QtGui.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.range_changed[int, int, str].emit( self._minimum[side], self._maximum[side], side) def setMinimum(self, minimum, side): self._minimum[side] = minimum self.update() self.range_changed[int, int, str].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.range_changed[int, int, str].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.value_changed[int, str].emit(val, side) def setPageStep(self, pagestep, side): if pagestep != self._pagestep[side]: self._pagestep[side] = pagestep self.update() self.page_step_changed[int, str].emit(pagestep, side) def syncPageStep(self): for side in ['left', 'right']: self.setPageStep(self._sbar[side].pageStep(), side) def resizeEvent(self, event): self.syncPageStep() 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[int].connect(self.setValue) sb.rangeChanged[int, int].connect(self.setRange) self.value_changed[int, str].connect( lambda v, s: side==s and sb.setValue(v)) self.range_changed[int, int, str].connect( lambda v1, v2, s: side==s and sb.setRange(v1, v2)) self.page_step_changed[int, str].connect( lambda v, s: side==s and sb.setPageStep(v)) hgview-1.9.0/hgviewlib/qt4/helpviewer.ui0000644000015700001640000000262512607505500021006 0ustar narvalnarval00000000000000 Dialog 0 0 400 300 Dialog Qt::Horizontal QDialogButtonBox::Close buttonBox accepted() Dialog accept() 248 254 157 274 buttonBox rejected() Dialog reject() 316 260 286 274 hgview-1.9.0/hgviewlib/qt4/revisions_table.py0000644000015700001640000005410412607505500022036 0ustar narvalnarval00000000000000# Copyright (c) 2013 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, see . # Copyright (c) 2009-2012 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, see . from operator import le, ge, lt, gt from mercurial import cmdutil, ui from mercurial.error import (RepoError, ParseError, LookupError, RepoLookupError, Abort) from PyQt4 import QtCore, QtGui from PyQt4.QtCore import Qt, pyqtSignal from hgviewlib.config import HgConfig from hgviewlib.hgpatches.scmutil import revrange from hgviewlib.util import tounicode from hgviewlib.qt4.mixins import ActionsMixin from hgviewlib.qt4.widgets import StyledTableView, SmartResizeTableView, \ QueryLineEdit from hgviewlib.qt4.quickbar import QuickBar from hgviewlib.qt4.helpviewer import HgHelpViewer from hgviewlib.qt4.styleditemdelegate import GraphItemDelegate class GotoQuery(QtCore.QThread): """A dedicated thread that queries a revset to the repo related to the model""" failed_revset = pyqtSignal(str) new_revset = pyqtSignal(tuple, str) def __init__(self): super(GotoQuery, self).__init__() self.rows = None self.revexp = None self.model = None def __del__(self): self.terminate() def run(self): revset = None try: revset = revrange(self.model.repo, [self.revexp.encode('utf-8')]) except (RepoError, ParseError, LookupError, RepoLookupError, Abort), err: self.rows = None self.failed_revset.emit(tounicode(err)) return if revset is None: self.rows = () self.new_revset.emit(self.rows, self.revexp) return rows = (idx.row() for idx in (self.model.indexFromRev(rev) for rev in revset) if idx is not None) self.rows = tuple(sorted(rows)) self.new_revset.emit(self.rows, self.revexp) def perform(self, revexp, model): self.terminate() self.revexp = revexp self.model = model self.start() def perform_now(self, revexp, model): self.revexp = revexp self.model = model self.run() def get_last_results(self): return self.rows class CompleterModel(QtGui.QStringListModel): def add_to_string_list(self, *values): strings = self.stringList() for value in values: if value not in strings: strings.append(value) self.setStringList(strings) class GotoQuickBar(QuickBar): goto_strict_next_from = pyqtSignal(tuple) goto_strict_prev_from = pyqtSignal(tuple) new_set = pyqtSignal([], [tuple]) goto_next_from = pyqtSignal(tuple) def __init__(self, parent, name='Goto'): self._parent = parent self._goto_query = None self.compl_model = None self.completer = None self.row_before = 0 self._standby_revexp = None # revexp that requires an action from user QuickBar.__init__(self, parent, name) def createActions(self): QuickBar.createActions(self) self.add_action( 'next', self.tr("Select Next"), icon='forward', tip=self.tr("Select the next matched revision"), callback=lambda: self.goto(forward=True), ) self.add_action( 'prev', self.tr("Select Previous"), icon='back', tip=self.tr("Select the Previous matched revision"), callback=lambda: self.goto(forward=False), ) self.add_action( 'help', self.tr("advanced help"), icon='help', tip=self.tr("Display avanced search help on 'revset'"), callback=self.show_help, ) def createContent(self): # completer self.compl_model = CompleterModel(['tip']) self.completer = QtGui.QCompleter(self.compl_model, self) cb = lambda text: self.search(text) self.completer.activated[str].connect(cb) # entry self.entry = QueryLineEdit(self) self.entry.setCompleter(self.completer) self.entry.setStatusTip("Enter a 'revset' to query a set of revisions") self.addWidget(self.entry) self.entry.text_edited_no_blank.connect(self.auto_search) self.entry.returnPressed.connect(lambda: self.goto(True)) # actions (better placed here) self.addActions(self.get_actions('prev', 'next', 'help')) # querier (threaded) self._goto_query = GotoQuery() self._goto_query.failed_revset.connect(self.on_failed) self._goto_query.new_revset.connect(self.on_queried) def setVisible(self, visible=True): QuickBar.setVisible(self, visible) if visible: self.entry.setFocus() self.entry.selectAll() def __del__(self): # QObject::startTimer: QTimer can only be used with threads # started with QThread self.entry.setCompleter(None) def show_help(self): w = HgHelpViewer(self._parent.model().repo, 'revset', self) w.show() w.raise_() w.activateWindow() def auto_search(self, revexp): # Do not automatically search for revision number. # The problem is that the auto search system will # query for lower revision number: users may type the revision # number by hand which induce that the first numeric char will be # queried alone. # But the first found revision is automatically selected, so to much # revision tree will be loaded. if revexp.isdigit(): self.entry.status = 'normal' self.set_actions('next', 'prev', enabled=True) self.show_message( 'Hit [Enter] because ' 'revision number is not automatically queried ' 'for optimization purpose.') self._standby_revexp = revexp return self.search(revexp) def goto(self, forward=True): # returnPressed from the `entry` also call this slot # We check if the main corresponding action is enabled if not self.get_action('next').isEnabled(): if self.entry.status == 'failed': self.show_message("Invalid revset expression.") else: self.show_message("Querying, please wait (or edit to cancel).") return if self._standby_revexp is not None: self.search(self._standby_revexp, threaded=False) rows = self._goto_query.get_last_results() if rows is None: self.entry.status = 'failed' return if forward: signal = self.goto_strict_next_from[tuple] else: signal = self.goto_strict_prev_from[tuple] signal.emit(rows) # usecase: enter a nodeid and hit enter to go on, # so the goto tool bar is no more required and may be # annoying if rows and len(rows) == 1: self.setVisible(False) def search(self, revexp, threaded=True): if revexp is None: revexp = self._standby_revexp self._standby_revexp = None if not revexp: self.new_set.emit() self.goto_next_from[tuple].emit((self.row_before,)) return self.show_message("Querying ... (edit the entry to cancel)") self.set_actions('next', 'prev', enabled=False) self.entry.status = 'query' if threaded: self._goto_query.perform(revexp, self._parent.model()) else: self._goto_query.perform_now(revexp, self._parent.model()) def show_message(self, message, delay=-1): self.parent().statusBar().showMessage(message, delay) def on_queried(self, rows=None, revexp=u''): """Slot to handle new revset.""" self.entry.status = 'valid' self.new_set[tuple].emit(rows) self.goto_next_from[tuple].emit(rows) self.set_actions('next', 'prev', enabled=True) if rows and revexp: self.compl_model.add_to_string_list(revexp) def on_failed(self, err): self.entry.status = 'failed' self.show_message(tounicode(err)) self.set_actions('next', 'prev', enabled=False) class RevisionsTableView(ActionsMixin, StyledTableView, SmartResizeTableView): """ A QTableView for displaying a FileRevModel or a HgRepoListModel, with actions, shortcuts, etc. """ start_from_rev = pyqtSignal([], [int, bool], [str, bool]) revision_activated = pyqtSignal([], [int]) revision_selected = pyqtSignal([], [int]) message_logged = pyqtSignal(str, int) def __init__(self, parent=None): super(RevisionsTableView, self).__init__(parent) self.init_variables() self.setShowGrid(False) self.verticalHeader().hide() self.verticalHeader().setDefaultSectionSize(20) self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) self.setAlternatingRowColors(True) self.createActions() self.createToolbars() self.doubleClicked.connect(self.revisionActivated) self.styled_item_delegate = GraphItemDelegate(self) def mousePressEvent(self, event): index = self.indexAt(event.pos()) if not index.isValid(): return self.pointed_rev = self.revFromindex(self.indexAt(event.pos())) if event.button() == Qt.MidButton: self.gotoAncestor() return elif event.button() == Qt.LeftButton: super(RevisionsTableView, self).mousePressEvent(event) def createToolbars(self): self.goto_toolbar = GotoQuickBar(self) goto = self.on_goto_next_from self.goto_toolbar.goto_strict_next_from[tuple].connect( lambda revs: goto(revs, strict=True, forward=True)) self.goto_toolbar.goto_strict_prev_from[tuple].connect( lambda revs: goto(revs, strict=True, forward=False)) self.goto_toolbar.goto_next_from[tuple].connect(goto) self.goto_toolbar.new_set.connect(self.highlight_rows) self.goto_toolbar.new_set[tuple].connect(self.highlight_rows) def createActions(self): self.add_action( 'copycs', self.tr("Export to clipboard"), menu=self.tr("Content"), tip=self.tr("Export changeset metadata the window manager " "clipboard [see configuration entry " "'exporttemplate']"), callback=self.copy_cs_to_clipboard, ) self.add_action( 'manifest', self.tr("Manifest"), menu=self.tr("Content"), tip=self.tr("Show the manifest at selected revision"), keys=[Qt.SHIFT + Qt.Key_Enter, Qt.SHIFT + Qt.Key_Return], callback=self.showAtRev, ) self.add_action( 'back', self.tr("Previous visited"), menu=self.tr("Select"), icon='back', tip=self.tr("Backward to the previous visited changeset"), keys=[QtGui.QKeySequence(QtGui.QKeySequence.Back)], callback=self.back, ) self.add_action( 'forward', self.tr("Next visited"), menu=self.tr("Select"), icon='forward', tip=self.tr("Forward to the next visited changeset"), keys=[QtGui.QKeySequence(QtGui.QKeySequence.Forward)], callback=self.forward, ) self.add_action( 'gotoancestor', self.tr('ancestor of this and selected'), menu=self.tr("Select"), icon='goto', tip=self.tr("Select the common ancestor of current pointed " "and selected revisions"), callback=self.gotoAncestor, ) self.add_action( 'start', self.tr("Hide higher revisions"), menu=self.tr("Filter"), tip=self.tr("Start graph from this revision"), keys=[Qt.Key_Backspace], callback=self.startFromRev, ) self.add_action( 'follow', self.tr("Show ancestors only"), menu=self.tr("Filter"), tip=self.tr("Follow revision history from this revision"), keys=[Qt.SHIFT + Qt.Key_Backspace], callback=self.followFromRev, ) self.add_action( 'unfilter', self.tr("Show all changesets"), menu=self.tr("Filter"), icon='unfilter', tip=self.tr("Remove filter and show all changesets"), keys=[Qt.ALT + Qt.CTRL + Qt.Key_Backspace], callback=self.removeFilter, enabled=False, ) self.start_from_rev.connect(self.update_filter_action) self.start_from_rev[int, bool].connect(self.update_filter_action) self.start_from_rev[str, bool].connect(self.update_filter_action) def update_filter_action(self, rev=None, follow=None): self.set_action('unfilter', enabled=rev is not None) def copy_cs_to_clipboard(self): """ Copy changeset metadata into the window manager clipboard.""" repo = self.model().repo rev = self.pointed_rev if self.pointed_rev is not None else self.current_rev ctx = repo[rev] u = ui.ui(repo.ui) template = HgConfig(u).getExportTemplate() u.pushbuffer() cmdutil.show_changeset(u, repo, {'template':template}, False).show(ctx) QtGui.QApplication.clipboard().setText(u.popbuffer()) def showAtRev(self): rev = self.pointed_rev if self.pointed_rev is not None else self.current_rev if rev is None: self.revision_activated.emit() else: self.revision_activated[int].emit(rev) def startFromRev(self): rev = self.current_rev if self.pointed_rev is None else self.pointed_rev if rev is None: self.start_from_rev.emit() else: self.start_from_rev[int, bool].emit(rev, False) def followFromRev(self): rev = self.pointed_rev if self.pointed_rev is not None else self.current_rev if rev is None: self.start_from_rev.emit() else: self.start_from_rev[int, bool].emit(rev, True) def removeFilter(self): self.start_from_rev.emit() def init_variables(self): # member variables self.current_rev = None # revision selected for which details are shown self.pointed_rev = None # revision pointed by mouse but not selected # rev navigation history (manage 'back' action) self._rev_history = [] self._rev_pos = -1 self._in_history = False # flag set when we are "in" the # history. It is required cause we cannot known, in # "revision_selected", if we are creating a new branch in the # history navigation or if we are navigating the history def setModel(self, model): self.init_variables() super(RevisionsTableView, self).setModel(model) self.selectionModel().currentRowChanged.connect(self.revisionSelected) tags = (tounicode(tag) for tag in model.repo.tags().keys()) self.goto_toolbar.compl_model.add_to_string_list(*tags) revaliases = [tounicode(item[0]) for item in model.repo.ui.configitems("revsetalias")] self.goto_toolbar.compl_model.add_to_string_list(*revaliases) col = list(model._columns).index('Log') self.horizontalHeader().setResizeMode(col, QtGui.QHeaderView.Stretch) def is_styled_column(self, index): return self.model()._columns[index] == 'Log' def get_column_stretch(self, index): model = self.model() return model._stretchs.get(model._columns[index]) 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 revisionActivated(self, index): rev = self.revFromindex(index) if rev is not None: self.revision_activated[int].emit(rev) def revisionSelected(self, index, index_from): """ Callback called when a revision is selected in the revisions table """ rev = self.revFromindex(index) if True:#rev is not None: model = self.model() if self.current_rev is not None and self.current_rev == rev: return if 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 if rev is None: self.revision_selected.emit() else: self.revision_selected[int].emit(rev) self.set_navigation_button_state() def gotoAncestor(self): """goto and select the common ancestor of self.pointed_rev and self.current_rev.""" repo = self.model().repo pointed = self.pointed_rev if self.pointed_rev is not None else repo[None].rev() current = self.current_rev if self.current_rev is not None else repo[None].rev() ctx = repo[current] ctx2 = repo[pointed] ancestor = ctx.ancestor(ctx2) self.message_logged.emit( "Goto ancestor of %s and %s"%(ctx.rev(), ctx2.rev()), 5000) self.goto(ancestor.rev()) def set_navigation_button_state(self): if len(self._rev_history) > 0: back = self._rev_pos > 0 forw = self._rev_pos < len(self._rev_history)-1 else: back = False forw = False self.set_action('back', enabled=back) self.set_action('forward', enabled=forw) def back(self): if self._rev_history and self._rev_pos>0: self._rev_pos -= 1 idx = self.model().indexFromRev(self._rev_history[self._rev_pos]) if idx is not None: self._in_history = True self.setCurrentIndex(idx) self.set_navigation_button_state() def forward(self): if self._rev_history and self._rev_pos<(len(self._rev_history)-1): self._rev_pos += 1 idx = self.model().indexFromRev(self._rev_history[self._rev_pos]) if idx is not None: self._in_history = True self.setCurrentIndex(idx) self.set_navigation_button_state() def goto(self, rev=None): """ Select revision 'rev'. It can be anything understood by repo.changectx(): revision number, node or tag for instance. """ if isinstance(rev, basestring) and ':' in rev: rev = rev.split(':')[1] repo = self.model().repo try: rev = repo.changectx(rev).rev() except RepoError: self.message_logged.emit( "Can't find revision '%s'" % rev, 2000) else: idx = self.model().indexFromRev(rev) if idx is not None: self.goto_toolbar.setVisible(False) self.setCurrentIndex(idx) def on_goto_next_from(self, rows, strict=False, forward=True): """Select the next row available in rows.""" if not rows: return currow = self.currentIndex().row() if strict: greater, less = gt, lt else: greater, less = ge, le if forward: comparer, _rows = greater, rows else: comparer, _rows = less, reversed(rows) try: row = (row for row in _rows if comparer(row, currow)).next() except StopIteration: self.visual_bell() row = rows[0 if forward else -1] self.setCurrentIndex(self.model().index(row, 0)) pos = rows.index(row) + 1 self.message_logged.emit( "revision #%i of %i" % (pos, len(rows)), -1) def nextRev(self): row = self.currentIndex().row() self.setCurrentIndex(self.model().index(min(row+1, self.model().rowCount() - 1), 0)) def prevRev(self): row = self.currentIndex().row() self.setCurrentIndex(self.model().index(max(row - 1, 0), 0)) def highlight_rows(self, rows=None): if rows is None: self.visual_bell() self.message_logged.emit('Revision set cleared.', 2000) else: self.message_logged.emit( '%i revisions found.' % len(rows), 2000) self.model().highlight_rows(rows or ()) self.refresh_display() def refresh_display(self): for item in self.children(): try: item.update() except AttributeError: pass def visual_bell(self): self.hide() QtCore.QTimer.singleShot(0.01, lambda: self.show()) hgview-1.9.0/hgviewlib/qt4/hgfileview.py0000644000015700001640000006353512607505500021007 0ustar narvalnarval00000000000000# Copyright (c) 2009-2012 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, see . """ Qt4 high level widgets for hg repo changelogs and filelogs """ import os import difflib from itertools import imap import tempfile try: from mercurial.error import LookupError, ManifestLookupError except ImportError: # ManifestLookupError is missing in older versions from mercurial.revlog import LookupError, LookupError as ManifestLookupError from PyQt4 import QtCore, QtGui, Qsci from PyQt4.QtCore import Qt, pyqtSignal from hgviewlib.util import exec_flag_changed, isbfile, bfilepath, tounicode from hgviewlib.config import HgConfig from hgviewlib.qt4.mixins import ActionsMixin from hgviewlib.qt4.config import get_font from hgviewlib.qt4.blockmatcher import BlockList from hgviewlib.qt4.widgets import SourceViewer, Annotator class HgQsci(ActionsMixin, SourceViewer): def __init__(self, *args, **kwargs): super(HgQsci, self).__init__(*args, **kwargs) self.createActions() def createActions(self): self.add_action( "diffmode", self.tr("Diff mode"), menu=self.tr("Mode"), icon='diffmode' , tip=self.tr('Enable/Disable Diff mode'), checked=True, ) self.add_action( "ignorews", self.tr("Ignore all space"), menu=self.tr("Mode"), tip=self.tr("Ignore all space"), checked=True, ) self.add_action( "show-big-file", self.tr('Display heavy file'), menu=self.tr("Mode"), icon='heavy', tip=self.tr('Display file Content even if it is marked as too big' '[config: maxfilesize]'), checked=False, ) self.add_action( "annmode", self.tr("Annotate mode"), menu=self.tr("Mode"), tip=self.tr('Enable/Disable Annotate mode'), checked=True, ) act = self.add_action( "openexternal", self.tr("Open in external application"), menu=self.tr("View"), tip=self.tr("Open file in an external application at the current " "revision"), ) self.add_action( "next", self.tr('Next hunk'), menu=self.tr("Moves"), icon='down', tip=self.tr('Jump to the next hunk'), keys=[Qt.ALT + Qt.Key_Down] ) self.add_action( "prev", self.tr('Prior hunk'), menu=self.tr("Moves"), icon='up', tip=self.tr('Jump to the previous hunk'), keys=[Qt.ALT + Qt.Key_Up] ) def toggle_openexternal(self, status=None): openexternal = self.get_action('openexternal') if status is None: status = not openexternal.isEnabled() openexternal.setEnabled(status) class HgFileView(ActionsMixin, QtGui.QFrame): filled = pyqtSignal() message_logged = pyqtSignal(str, int) rev_for_diff_changed = pyqtSignal(int) def __init__(self, parent=None): self._diff = None self._diffs = None self.cfg = None super(HgFileView, self).__init__(parent) framelayout = QtGui.QVBoxLayout(self) framelayout.setContentsMargins(0, 0, 0, 0) framelayout.setSpacing(0) self.info_frame = QtGui.QFrame() framelayout.addWidget(self.info_frame) l = QtGui.QVBoxLayout() self.info_frame.setLayout(l) self.filenamelabel = QtGui.QLabel() self.filenamelabel.setWordWrap(True) self.filenamelabel.setTextInteractionFlags( QtCore.Qt.TextSelectableByKeyboard| QtCore.Qt.TextSelectableByMouse| QtCore.Qt.LinksAccessibleByMouse) self.filenamelabel.linkActivated.connect( lambda link: self.displayFile(show_big_file=True)) self.execflaglabel = QtGui.QLabel() self.execflaglabel.setWordWrap(True) l.addWidget(self.filenamelabel) l.addWidget(self.execflaglabel) self.execflaglabel.hide() self.filedata_frame = QtGui.QFrame() framelayout.addWidget(self.filedata_frame) l = QtGui.QHBoxLayout() l.setContentsMargins(0,0,0,0) l.setSpacing(0) self.filedata_frame.setLayout(l) self.sci = HgQsci(self) l.addWidget(self.sci, 1) ll = QtGui.QVBoxLayout() ll.setContentsMargins(0, 0, 0, 0) ll.setSpacing(0) l.insertLayout(0, ll) ll2 = QtGui.QHBoxLayout() ll2.setContentsMargins(0, 0, 0, 0) ll2.setSpacing(0) ll.addLayout(ll2) # used to fill height of the horizontal scroll bar w = QtGui.QWidget(self) ll.addWidget(w) self._spacer = w self.blk = BlockList(self) self.blk.linkScrollBar(self.sci.verticalScrollBar()) ll2.addWidget(self.blk) self.blk.setVisible(False) self.ann = Annotator(self.sci, self) ll2.addWidget(self.ann) self.ann.setVisible(False) self._model = None self._ctx = None self._filename = None self._annotate = False self._find_text = None self._mode = "diff" # can be 'diff' or 'file' self.filedata = None self.timer = QtCore.QTimer() self.timer.setSingleShot(False) self.timer.timeout.connect(self.idle_fill_files) self.sci.set_action('diffmode', callback=self.setMode) self.sci.set_action('ignorews', callback=lambda value: self.setUiConfig('diff', 'ignorews', value)) self.sci.set_action('annmode', callback=self.setAnnotate) self.sci.set_action('prev', callback=self.prevDiff) self.sci.set_action('next', callback=self.nextDiff) self.sci.set_action('show-big-file', callback=self.showBigFile) self.sci.set_action('openexternal', callback=self.openexternal) self.sci.set_action('diffmode', checked=True) def resizeEvent(self, event): super(HgFileView, self).resizeEvent(event) h = self.sci.horizontalScrollBar().height() self._spacer.setMinimumHeight(h) self._spacer.setMaximumHeight(h) def showBigFile(self, state): """Force displaying the content related to a file considered previously as too big. """ if not self._model.graph: return if not state: self._model.graph.maxfilesize = self.cfg.getMaxFileSize() else: self._model.graph.maxfilesize = -1 self.displayFile() def setMode(self, mode): if isinstance(mode, bool): mode = ['file', 'diff'][mode] assert mode in ('diff', 'file') self.sci.set_actions('annmode', 'next', 'prev', enabled=not mode) if mode != self._mode: self._mode = mode self.blk.setVisible(self._mode == 'file') self.ann.setVisible(self._mode == 'file' and self._annotate) self.displayFile() def setUiConfig(self, section, name, value): if self._model.repo.ui._tcfg.get(section, name) == value: return self._model.repo.ui._tcfg.set(section, name, value, source='hgview') self.displayFile() def setAnnotate(self, ann): self._annotate = ann if ann: self.displayFile() def setModel(self, model): # XXX we really need only the "Graph" instance self._model = model self.cfg = HgConfig(self._model.repo.ui) if self._model.graph: is_show_big_file = self._model.graph.maxfilesize < 0 else: is_show_big_file = bool(self.cfg.getMaxFileSize()) self.sci.set_action('show-big-file', checked=is_show_big_file) self.sci.set_action('ignorews', checked=model.repo.ui.configbool('diff', 'ignorews')) self.sci.setFont(get_font(self.cfg)) self.sci.clear() def setContext(self, ctx): self._ctx = ctx self._p_rev = None self.sci.clear() def rev(self): return self._ctx.rev() def filename(self): return self._filename def displayDiff(self, rev): if rev != self._p_rev: self.displayFile(rev=rev) def displayFile(self, filename=None, rev=None, show_big_file=None): if filename is None: filename = self._filename self._realfilename = filename if isbfile(filename): self._filename = bfilepath(filename) else: self._filename = filename if rev is not None: self._p_rev = rev self.rev_for_diff_changed.emit(rev) self.sci.clear() self.ann.clear() self.filenamelabel.setText(" ") self.execflaglabel.clear() if filename is None: return try: filectx = self._ctx.filectx(self._realfilename) except (LookupError, ManifestLookupError): # occur on deleted files self.sci.toggle_openexternal(status=False) except HgLookupError: # occur on deleted files self.sci.toggle_openexternal(status=False) return if self._mode == 'diff' and self._p_rev is not None: mode = self._p_rev else: mode = self._mode if show_big_file: flag, data = self._model.graph.filedata(filename, self._ctx.rev(), mode, maxfilesize=-1) else: flag, data = self._model.graph.filedata(filename, self._ctx.rev(), mode) if data and data[-1] == '\n': data = data[:-1] if flag == 'file too big': self.filedata_frame.hide() message = (('
' 'File size (%s) greater than configured maximum value: ' ' maxfilesize=%i
' '
' 'Click to display anyway ' '.' '
') % (data, self.cfg.getMaxFileSize())) self.filenamelabel.setText(message) return else: self.filedata_frame.show() if flag == '-' or flag == '': self.sci.toggle_openexternal(status=False) return self.sci.toggle_openexternal(status=True) if data not in (u'file too big', u'binary file'): self.filedata = data else: self.filedata = None exec_flag = exec_flag_changed(filectx) if exec_flag: self.execflaglabel.setText(u"exec mode has been %s" % exec_flag) self.execflaglabel.show() else: self.execflaglabel.hide() labeltxt = u'' if isbfile(self._realfilename): labeltxt += u'[bfile tracked] ' labeltxt += u"%s" % tounicode(self._filename) if self._p_rev is not None: labeltxt += u' (diff from rev %s)' % self._p_rev renamed = filectx.renamed() if renamed: labeltxt += u' (renamed from %s)' % tounicode(bfilepath(renamed[0])) self.filenamelabel.setText(labeltxt) self.sci.set_text(filename, data, flag, self.cfg) if self._find_text: self.highlightSearchString(self._find_text) self.sci.set_action('prev', enabled=False) self.updateDiffDecorations() if self._mode == 'file' and self._annotate: if filectx.rev() is None: # XXX hide also for binary files self.ann.setVisible(False) else: self.ann.setVisible(self._annotate) self.ann.setFont(self.sci.font()) self.ann.set_line_ticks([str(f.rev()) for f, __ in filectx.annotate(follow=True)]) return True def openexternal(self): """Open the external application with the content of the selected file at the selected revision""" # We open the current file if the selected revision is the dirty working # directory or if it is the working directory without any modification. # Else we use a temporary file. content_getter = lambda: self._model.graph.filedata( self._filename, self._ctx.rev(), 'file', maxfilesize=-1)[1] _open_in_external(self, self.cfg, self._ctx.filectx(self._filename), content_getter) def updateDiffDecorations(self): """ Recompute the diff and starts the timer responsible for filling diff decoration markers """ self.blk.clear() if self._mode == 'file' and self.filedata is not None: if self.timer.isActive(): self.timer.stop() parent = self._model.graph.fileparent(self._filename, self._ctx.rev()) if parent is None: return m = self._ctx.filectx(self._filename).renamed() if m: pfilename, __ = m else: pfilename = self._filename _, parentdata = self._model.graph.filedata(pfilename, parent, 'file') if parentdata is not None: filedata = self.filedata.splitlines() parentdata = parentdata.splitlines() self._diff = difflib.SequenceMatcher(None, parentdata, filedata,) self._diffs = [] self.blk.syncPageStep() self.timer.start() def _nextDiff(self): if self._mode == 'file': row, __ = self.sci.getCursorPosition() lo = 0 for i, (lo, __) in enumerate(self._diffs): if lo > row: last = (i == (len(self._diffs)-1)) break else: return False self.sci.setCursorPosition(lo, 0) self.sci.verticalScrollBar().setValue(lo) return not last def nextDiff(self): notlast = self._nextDiff() self.sci.set_action('next', enabled=self.fileMode() and notlast and self.nDiffs()) self.sci.set_action('prev', enabled=self.fileMode() and self.nDiffs()) def _prevDiff(self): if self._mode == 'file': row, __ = self.sci.getCursorPosition() lo = 0 for i, (lo, hi) in enumerate(reversed(self._diffs)): if hi < row: first = (i == (len(self._diffs)-1)) break else: return False self.sci.setCursorPosition(lo, 0) self.sci.verticalScrollBar().setValue(lo) return not first def prevDiff(self): notfirst = self._prevDiff() self.sci.set_action('prev', enabled=self.fileMode() and notfirst and self.nDiffs()) self.sci.set_action('next', enabled=self.fileMode() and self.nDiffs()) def nextLine(self): x, y = self.sci.getCursorPosition() self.sci.setCursorPosition(x+1, y) def prevLine(self): x, y = self.sci.getCursorPosition() self.sci.setCursorPosition(x-1, y) def nextCol(self): x, y = self.sci.getCursorPosition() self.sci.setCursorPosition(x, y+1) def prevCol(self): x, y = self.sci.getCursorPosition() self.sci.setCursorPosition(x, y-1) def nDiffs(self): return len(self._diffs) def diffMode(self): return self._mode == 'diff' def fileMode(self): return self._mode == 'file' def searchString(self, text): self._find_text = text self.sci.clear_highlights() findpos = self.highlightSearchString(self._find_text) if findpos: def finditer(self, findpos): if self._find_text: for pos in findpos: self.sci.highlight_current_search_string(pos, self._find_text) yield self._ctx.rev(), self._filename, pos return finditer(self, findpos) def highlightSearchString(self, text): pos = self.sci.search_and_highlight_string(text) msg = u"Found %d occurrences of '%s' in current file or diff" % \ (len(pos), tounicode(text)) self.message_logged.emit(msg, 2000) return pos def verticalScrollBar(self): return self.sci.verticalScrollBar() 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 self.sci.setUpdatesEnabled(False) self.blk.setUpdatesEnabled(False) for __ in range(30): # burst pool if self._diff is None or not self._diff.get_opcodes(): self._diff = None self.timer.stop() self.filled.emit() self.sci.set_action('next', enabled=self.fileMode() and self.nDiffs()) break tag, __, __, blo, bhi = self._diff.get_opcodes().pop(0) if tag == 'replace': self._diffs.append([blo, bhi]) self.blk.addBlock('x', blo, bhi) for i in range(blo, bhi): self.sci.markerAdd(i, self.sci.markertriangle) elif tag == 'delete': pass elif tag == 'insert': self._diffs.append([blo, bhi]) self.blk.addBlock('+', blo, bhi) for i in range(blo, bhi): self.sci.markerAdd(i, self.sci.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 self.sci.setUpdatesEnabled(True) self.blk.setUpdatesEnabled(True) class HgFileListView(ActionsMixin, QtGui.QTableView): """ A QTableView for displaying a HgFileListModel """ file_selected = pyqtSignal([str, int], [str]) def __init__(self, parent=None): super(HgFileListView, self).__init__(parent) self.setShowGrid(False) self.verticalHeader().hide() self.verticalHeader().setDefaultSectionSize(20) self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) self.setAlternatingRowColors(True) self.setTextElideMode(Qt.ElideLeft) self.horizontalHeader().setToolTip('Double click to toggle merge mode') self.createActions() self.horizontalHeader().sectionDoubleClicked[int].connect( self.toggleFullFileList) self.doubleClicked.connect(self.fileActivated) self.horizontalHeader().sectionResized[int, int, int].connect( self.sectionResized) self._diff_dialogs = {} self._nav_dialogs = {} def setModel(self, model): super(HgFileListView, self).setModel(model) model.layoutChanged.connect(self.fileSelected) self.selectionModel().currentRowChanged.connect( self.fileSelected) self.horizontalHeader().setResizeMode(1, QtGui.QHeaderView.Stretch) def currentFile(self): index = self.currentIndex() return self.model().fileFromIndex(index) def toggle_openexternal(self, status=None): openexternal = self.get_action('openexternal') if status is None: status = not openexternal.isEnabled() openexternal.setEnabled(status) def fileSelected(self, index=None, *args): if index is None: index = self.currentIndex() sel_file = self.model().fileFromIndex(index) from_rev = self.model().revFromIndex(index) if sel_file is not None: self.toggle_openexternal(self.model().fileflag(sel_file) != '-') # signal get unicode as input if from_rev is None: self.file_selected[str].emit(tounicode(sel_file)) else: self.file_selected[str, int].emit(tounicode(sel_file), from_rev) def selectFile(self, filename): self.setCurrentIndex(self.model().indexFromFile(filename)) def fileActivated(self, index, alternate=False): sel_file = self.model().fileFromIndex(index) if sel_file is '': return if alternate: self.navigate(sel_file) else: self.diffNavigate(sel_file) def toggleFullFileList(self, *args): self.model().toggleFullFileList() def openexternal(self): """Open an external application with the content of the selected file at the selected revision""" index = self.currentIndex() sel_file = self.model().fileFromIndex(index) from_rev = self.model().current_ctx.rev() try: filectx = self.model().repo[from_rev].filectx(sel_file) except (LookupError, ManifestLookupError): return _open_in_external(self, HgConfig(self.model().repo.ui), filectx, filectx.data) def navigate(self, filename=None): from hgviewlib.qt4.hgfiledialog import FileViewer self._navigate(filename, FileViewer, self._nav_dialogs) def diffNavigate(self, filename=None): from hgviewlib.qt4.hgfiledialog import FileDiffViewer self._navigate(filename, FileDiffViewer, self._diff_dialogs) def _navigate(self, filename, dlgclass, dlgdict): if filename is None: filename = self.currentFile() model = self.model() if filename and len(model.repo.file(filename))>0: if filename not in dlgdict: dlg = dlgclass(model.repo, filename, repoviewer=self.window()) dlgdict[filename] = dlg dlg.setWindowTitle('Hg file log viewer') dlg = dlgdict[filename] dlg.goto(model.current_ctx.rev()) dlg.show() dlg.raise_() dlg.activateWindow() def createActions(self): self.add_action( 'navigate', self.tr("Navigate"), menu=self.tr("navigate"), tip=self.tr('Navigate the revision tree of this file'), callback=lambda: self.navigate(), ) self.add_action( 'diffnavigate', self.tr("Diff-mode navigate"), menu=self.tr("navigate"), tip=self.tr('Navigate the history of this file in diff mode'), callback=lambda: self.diffNavigate(), ) self.add_action( "openexternal", self.tr("Open in external application"), menu=self.tr("View"), tip=self.tr("Open file in an external application at the current " "revision"), callback=self.openexternal, ) def resizeEvent(self, event): vp_width = self.viewport().width() col_widths = [self.columnWidth(i) \ for i in range(1, self.model().columnCount())] col_width = vp_width - sum(col_widths) col_width = max(col_width, 50) self.setColumnWidth(0, col_width) QtGui.QTableView.resizeEvent(self, event) def sectionResized(self, idx, oldsize, newsize): if idx == 1: self.model().setDiffWidth(newsize) def nextFile(self): row = self.currentIndex().row() self.setCurrentIndex(self.model().index(min(row+1, self.model().rowCount() - 1), 0)) def prevFile(self): row = self.currentIndex().row() self.setCurrentIndex(self.model().index(max(row - 1, 0), 0)) def _open_in_external(parent, cfg, filectx, content_getter): """Open an external application on the file at the context ``filectx``. If the selected revision is the working directory then the original file opened else a temporary file is created. """ if filectx.rev() is None: # dirty wd is selected return _open_originalfile_in_external(parent, filectx) if ((not filectx._repo[None].dirty()) and # the wd is clean and (filectx.changectx() in filectx._repo[None].parents())): # a wd parent is selected return _open_originalfile_in_external(parent, filectx) content = content_getter() suffix = '_%s-%s-%s' % (filectx.rev(), str(filectx.changectx()), os.path.basename(filectx.path())) _open_tempfile_in_external(parent, content, suffix=suffix) def _open_originalfile_in_external(parent, filectx): """Open an external application on the original file from filectx""" filepath = os.path.join( os.path.abspath(filectx._repo.root), filectx.path() ) return QtGui.QDesktopServices.openUrl(QtCore.QUrl(filepath)) def _open_tempfile_in_external(parent, content, suffix=None): """Open an external application with the given ``content`` in a temporary file. ``suffix`` is the suffix for the temporary file name. ``parent`` is a Qt component that gets a reference to the editor process. """ fid, filepath = tempfile.mkstemp(suffix=suffix) os.close(fid) with open(filepath, 'w') as fid: fid.write(content) return QtGui.QDesktopServices.openUrl(QtCore.QUrl(filepath)) hgview-1.9.0/hgviewlib/qt4/hgrepomodel.py0000644000015700001640000006772412607505500021167 0ustar narvalnarval00000000000000# Copyright (c) 2009-2012 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, see . """ Qt4 model for hg repo changelogs and filelogs """ import sys import re import os, os.path as osp from functools import partial from mercurial.node import nullrev from mercurial.node import hex, short as short_hex from mercurial.revlog import LookupError from mercurial import util, error from hgviewlib.hggraph import Graph, ismerge, diff as revdiff, HgRepoListWalker from hgviewlib.hggraph import revision_grapher, filelog_grapher, getlog, gettags from hgviewlib.config import HgConfig from hgviewlib.util import tounicode, isbfile, xml_escape, allbranches from hgviewlib.qt4 import icon as geticon from hgviewlib.decorators import timeit from hgviewlib.hgpatches import phases from PyQt4.QtGui import QColor, QPixmap, QPainter, QPen, QFont from PyQt4.QtCore import Qt, pyqtSignal, QAbstractItemModel, QAbstractTableModel, \ QObject, QDateTime, QTimer, QModelIndex, QPointF # XXX make this better than a poor hard written list... COLORS = [ "blue", "darkgreen", "red", "green", "darkblue", "purple", "cyan", Qt.darkYellow, "magenta", "darkred", "darkmagenta", "darkcyan", "gray", ] COLORS = [str(QColor(x).name()) for x in COLORS] #COLORS = [str(color) for color in QColor.colorNames()] # We use two colors, One for even rows and one for odd rows COLOR_BG_OBSOLETE = [QColor(255, 250, 250), QColor(243, 230, 230)] COLOR_BG_TROUBLED = [QColor(255, 193, 71), QColor(255, 153, 51)] COLOR_BG_HIGHLIGHT = [QColor(127, 199, 175), QColor(127, 199, 175).lighter()] BOOKMARK_CSS = "color: white; background-color: blue;" TAG_CSS = "color: black; background-color: SandyBrown;" def cvrt_date(date): """ Convert a date given the hg way, ie. couple (date, tz), into a formatted string """ if not date: return u'' date, tzdelay = date return QDateTime.fromTime_t(int(date)).toString(Qt.LocaleDate) # XXX maybe it's time to make these methods of the model... # in following lambdas, ctx is a hg changectx _columnmap = {'ID': lambda model, ctx, gnode: ctx.rev() is not None and tounicode(ctx.rev()) or u"", 'Log': getlog, 'Author': lambda model, ctx, gnode: tounicode(ctx.user()), 'Date': lambda model, ctx, gnode: cvrt_date(ctx.date()), 'Branch': lambda model, ctx, gnode: tounicode(ctx.branch()), 'Filename': lambda model, ctx, gnode: tounicode(gnode.extra[0]), 'Phase': lambda model, ctx, gnode: tounicode(ctx.phasestr()), } _tooltips = {'ID': lambda model, ctx, gnode: ctx.rev() is not None and tounicode(ctx.hex()) or u"Working Directory", } def auth_width(model, repo): auths = model._aliases.values() if not auths: return None return sorted(auths, cmp=lambda x,y: cmp(len(x), len(y)))[-1] # in following lambdas, r is a hg repo # it return the longuest entry of this column _maxwidth = {'ID': lambda self, r: str(len(r.changelog)), 'Date': lambda self, r: cvrt_date(r.changectx(0).date()), 'Branch': lambda self, r: ([None] + sorted(allbranches(r), key=len))[-1], 'Author': lambda self, r: 'author name', 'Filename': lambda self, r: self.filename, 'Phase': lambda self, r: sorted(phases.phasenames, key=len)[-1] } def datacached(meth): """ decorator used to cache 'data' method of Qt models. It will *not* cache None return values (so costly non-null values can be computed and filled as a background process) """ def data(self, index, role): if not index.isValid(): return None row = index.row() col = index.column() if (row, col, role) in self._datacache: return self._datacache[(row, col, role)] result = meth(self, index, role) if result is not None: self._datacache[(row, col, role)] = result return result return data class HgRepoListModel(QAbstractTableModel, HgRepoListWalker): """ Model used for displaying the revisions of a Hg *local* repository """ message_logged = pyqtSignal(str, int) filled = pyqtSignal() _allcolumns = ('ID', 'Branch', 'Log', 'Author', 'Date') _columns = ('ID', 'Branch', 'Log', 'Author', 'Date') _stretchs = {'Log': 1, } _getcolumns = "getChangelogColumns" def __init__(self, repo, branch='', fromhead=None, follow=False, parent=None, show_hidden=False, closed=False): """ repo is a hg repo instance """ self._fill_timer = None QAbstractTableModel.__init__(self, parent) HgRepoListWalker.__init__(self, repo, branch, fromhead, follow, closed=closed) self.highlights = [] def setRepo(self, repo, branch='', fromhead=None, follow=False, closed=False): HgRepoListWalker.setRepo(self, repo, branch, fromhead, follow, closed=closed) self.layoutChanged.emit() QTimer.singleShot(0, lambda: self.filled.emit()) self._fill_timer = self.startTimer(50) def highlight_rows(self, rows): """mark ``rows`` to be highlighted.""" self.highlights[:] = rows self._datacache.clear() def timerEvent(self, event): if event.timerId() == self._fill_timer: self.message_logged.emit( 'filling (%s)' % (len(self.graph)), -1) if self.graph.isfilled(): self.killTimer(self._fill_timer) self._fill_timer = None self.updateRowCount() self.message_logged.emit('', -1) # we fill the graph data structures without telling # views until we are done - this gives # maximal GUI responsiveness elif not self.graph.build_nodes(nnodes=self.fill_step): self.killTimer(self._fill_timer) self._fill_timer = None self.updateRowCount() self.message_logged.emit('', -1) def updateRowCount(self): currentlen = self.rowcount newlen = len(self.graph) if newlen > self.rowcount: self.beginInsertRows(QModelIndex(), currentlen, newlen-1) self.rowcount = newlen self.endInsertRows() @staticmethod def get_color(n, ignore=()): """ Return a color at index 'n' rotating in the available colors. 'ignore' is a list of colors not to be chosen. """ ignore = [str(QColor(x).name()) for x in ignore] colors = [x for x in COLORS if x not in ignore] if not colors: # ghh, no more available colors... colors = COLORS return colors[n % len(colors)] def user_color(self, user): if user in self._aliases: user = self._aliases[user] if user in self._users: try: color = self._users[user]['color'] color = QColor(color).name() self._user_colors[user] = color except: pass return HgRepoListWalker.user_color(self, user) def _display_log(self, ctx, gnode, row): """Display the log column content.""" content = [] # display bookmarks bookmarks = [tounicode(bookmark) for bookmark in ctx.bookmarks()] if bookmarks: content.extend(u' %s ' % (BOOKMARK_CSS, xml_escape(bkm)) for bkm in bookmarks) # display tags tags = [tounicode(tag) for tag in gettags(self, ctx).split(',') if tag] if tags: content.extend(u' %s ' % (TAG_CSS, xml_escape(tag)) for tag in tags) # display log style = "color: grey;" if ctx.obsolete() else u"" content.append(u' %s ' % (style, xml_escape(_columnmap['Log'](self, ctx, gnode)))) return u' '.join(content) @datacached def data(self, index, role): if not index.isValid(): return None row = index.row() self.ensureBuilt(row=row) column = self._columns[index.column()] gnode = self.graph[row] ctx = self.repo.changectx(gnode.rev) if role == Qt.DisplayRole: if column == 'Author': #author user = _columnmap[column](self, ctx, gnode) if ctx.node() else u'' return self.user_name(user) elif column == 'Log': return self._display_log(ctx, gnode, row) return _columnmap[column](self, ctx, gnode) elif role == Qt.ToolTipRole: msg = u"Branch: %s
\n" % _columnmap['Branch'](self, ctx, gnode) msg += u"Phase: %s
\n" % _columnmap['Phase'](self, ctx, gnode) if gnode.rev in self.wd_revs: msg += u" Working Directory position" states = u'modified added removed deleted'.split() status = self.wd_status[self.wd_revs.index(gnode.rev)] status = [state for st, state in zip(status, states) if st] if status: msg += ' (%s)' % (', '.join(status)) msg += u"
\n" msg += _tooltips.get(column, _columnmap[column])(self, ctx, gnode) return msg elif role == Qt.ForegroundRole: color = None if column == 'Author': #author user = tounicode(ctx.user()) if ctx.node() else u'' color = QColor(self.user_color(user)) if ctx.obsolete(): color = color.lighter() elif column == 'Branch': #branch color = QColor(self.namedbranch_color(ctx.branch())) if ctx.obsolete(): color = color.lighter() elif ctx.obsolete(): color = QColor('grey') if color is not None: return color elif role == Qt.BackgroundRole: row = index.row() if row in self.highlights: return COLOR_BG_HIGHLIGHT[row % 2] elif ctx.obsolete(): return COLOR_BG_OBSOLETE[row % 2] elif ctx.troubles(): return COLOR_BG_TROUBLED[row % 2] def headerData(self, section, orientation, role): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self._columns[section] return None def maxWidthValueForColumn(self, column): column = self._columns[column] if column in _maxwidth: return _maxwidth[column](self, self.repo) return None def clear(self): """empty the list""" self.graph = None self._datacache = {} self.notify_data_changed() def notify_data_changed(self): self.layoutChanged.emit() def indexFromRev(self, rev): self.ensureBuilt(rev=rev) row = self.rowFromRev(rev) if row is not None: return self.index(row, 0) class FileRevModel(HgRepoListModel): """ Model used to manage the list of revisions of a file, in file viewer of in diff-file viewer dialogs. """ _allcolumns = ('ID', 'Branch', 'Log', 'Author', 'Date', 'Filename') _columns = ('ID', 'Branch', 'Log', 'Author', 'Date', 'Filename') _stretchs = {'Log': 1, } _getcolumns = "getFilelogColumns" def __init__(self, repo, filename=None, parent=None): """ data is a HgHLRepo instance """ HgRepoListModel.__init__(self, repo, parent=parent) self.setFilename(filename) def setRepo(self, repo, branch='', fromhead=None, follow=False, closed=False): self.repo = repo self._datacache = {} self.load_config() def setFilename(self, filename): self.filename = filename self._user_colors = {} self._branch_colors = {} self.rowcount = 0 self._datacache = {} if self.filename: grapher = filelog_grapher(self.repo, self.filename) self.graph = Graph(self.repo, grapher, self.max_file_size) fl = self.repo.file(self.filename) # we use fl.index here (instead of linkrev) cause # linkrev API changed between 1.0 and 1.?. So this # works with both versions. self.heads = [fl.index[fl.rev(x)][4] for x in fl.heads()] self.ensureBuilt(row=self.fill_step/2) QTimer.singleShot(0, lambda: self.filled.emit()) self._fill_timer = self.startTimer(500) else: self.graph = None self.heads = [] replus = re.compile(r'^[+][^+].*', re.M) reminus = re.compile(r'^[-][^-].*', re.M) class HgFileListModel(QAbstractTableModel): """ Model used for listing (modified) files of a given Hg revision """ _description_desc = dict(path='', flag='', desc='Display revision description', bfile=None, parent=None, fromside=None, infiles=False) def __init__(self, repo, parent=None): """ data is a HgHLRepo instance """ QAbstractTableModel.__init__(self, parent) self.repo = repo self._datacache = {} self.load_config() self.current_ctx = None self._files = [] self._filesdict = {} self.diffwidth = 100 self._fulllist = False self._fill_iter = None def toggleFullFileList(self): self._fulllist = not self._fulllist self.loadFiles() self.layoutChanged.emit() def load_config(self): cfg = HgConfig(self.repo.ui) self._flagcolor = {} self._flagcolor['='] = cfg.getFileModifiedColor() self._flagcolor['-'] = cfg.getFileRemovedColor() self._flagcolor['-'] = cfg.getFileDeletedColor() self._flagcolor['+'] = cfg.getFileAddedColor() self._flagcolor[''] = cfg.getFileDescriptionColor() self._displaydiff = cfg.getDisplayDiffStats() self._descriptionview = cfg.getFileDescriptionView() def setDiffWidth(self, w): if w != self.diffwidth: self.diffwidth = w self._datacache = {} self.dataChanged.emit( self.index(1, 0), self.index(1, self.rowCount())) def __len__(self): return len(self._files) def __contains__(self, filename): return filename in self._filesdict def rowCount(self, parent=None): return len(self) def columnCount(self, parent=None): return 1 + self._displaydiff def file(self, row): return self._files[row]['path'] def fileflag(self, fn): if not fn: return '+' return self._filesdict[fn]['flag'] def fileparentctx(self, fn, ctx=None): if ctx is None: return self._filesdict[fn]['parent'] return ctx.parents()[0] def fileFromIndex(self, index): if not index.isValid() or index.row()>=len(self) or not self.current_ctx: return None row = index.row() file_info = self._files[row] return self._files[row]['path'] def revFromIndex(self, index): if self._fulllist and ismerge(self.current_ctx): if not index.isValid() or index.row()>=len(self) or not self.current_ctx: return None row = index.row() if self._files[row]['fromside'] == 'right': return self.current_ctx.parents()[1].rev() return self.current_ctx.parents()[0].rev() return None def indexFromFile(self, filename): if filename in self._filesdict: row = self._files.index(self._filesdict[filename]) return self.index(row, 0) return QModelIndex() def _filterFile(self, filename, ctxfiles): if self._fulllist: return True return filename in ctxfiles #self.current_ctx.files() def _buildDesc(self, parent, fromside): _files = [] ctx = self.current_ctx ctxfiles = ctx.files() changes = self.repo.status(parent.node(), ctx.node())[:3] modified, added, removed = changes for lst, flag in ((added, '+'), (modified, '='), (removed, '-')): for f in [x for x in lst if self._filterFile(x, ctxfiles)]: desc = f bfile = isbfile(f) if bfile: desc = desc.replace('.hgbfiles'+os.sep, '') desc = tounicode(desc) _files.append({'path': f, 'flag': flag, 'desc': desc, 'bfile': bfile, 'parent': parent, 'fromside': fromside, 'infiles': f in ctxfiles}) # renamed/copied files are handled by background # filling process since it can be a bit long return _files def loadFiles(self): self._fill_iter = None self._files = [] self._datacache = {} self._files = self._buildDesc(self.current_ctx.parents()[0], 'left') if ismerge(self.current_ctx): _paths = [x['path'] for x in self._files] _files = self._buildDesc(self.current_ctx.parents()[1], 'right') self._files += [x for x in _files if x['path'] not in _paths] self._filesdict = dict([(f['path'], f) for f in self._files]) if self._descriptionview == 'asfile': self._files.insert(0, self._description_desc) self.fillFileStats() def setSelectedRev(self, ctx): if ctx != self.current_ctx: self.current_ctx = ctx self._datacache = {} self.loadFiles() self.layoutChanged.emit() def fillFileStats(self): """ Method called to start the background process of computing file stats, which are to be displayed in the 'Stats' column """ self._fill_iter = self._fill() self._fill_one_step() def _fill_one_step(self): if self._fill_iter is None: return try: nextfill = self._fill_iter.next() if nextfill is not None: row, col = nextfill idx = self.index(row, col) self.dataChanged.emit(idx, idx) QTimer.singleShot(10, lambda: self._fill_one_step()) except StopIteration: self._fill_iter = None def _fill(self): # the generator used to fill file stats as a background process files = enumerate(self._files) if self._descriptionview == 'asfile': files.next() # consume description entry for row, desc in files: filename = desc['path'] if desc['flag'] == '=' and self._displaydiff: try: diff = revdiff(self.repo, self.current_ctx, None, files=[filename]) tot = tounicode(self.current_ctx.filectx(filename).data()).count('\n') add = len(replus.findall(diff)) rem = len(reminus.findall(diff)) except (LookupError, TypeError): # unknown revision and mq support tot, add, rem = 0, 0, 0 if tot == 0: tot = max(add + rem, 1) desc['stats'] = (tot, add, rem) yield row, 1 if desc['flag'] == '+': m = self.current_ctx.filectx(filename).renamed() if m: removed = self.repo.status(desc['parent'].node(), self.current_ctx.node())[2] oldname, node = m if oldname in removed: # removed.remove(oldname) XXX desc['renamedfrom'] = (tounicode(oldname), node) desc['flag'] = '=' desc['desc'] += u'\n (was %s)' % tounicode(oldname) else: desc['copiedfrom'] = (tounicode(oldname), node) desc['flag'] = '=' desc['desc'] += u'\n (copy of %s)' % tounicode(oldname) yield row, 0 yield None def data(self, index, role): if not index.isValid() or index.row()>len(self) or not self.current_ctx: return None row = index.row() column = index.column() current_file_desc = self._files[row] current_file = current_file_desc['path'] stats = current_file_desc.get('stats') if column == 1: if stats is not None: if role == Qt.DecorationRole: tot, add, rem = stats w = self.diffwidth - 20 h = 20 np = int(w*add/tot) nm = int(w*rem/tot) nd = w-np-nm pix = QPixmap(w+10, h) pix.fill(QColor(0,0,0,0)) painter = QPainter(pix) for x0,w0, color in ((0, nm, 'red'), (nm, np, 'green'), (nm+np, nd, 'gray')): color = QColor(color) painter.setBrush(color) painter.setPen(color) painter.drawRect(x0+5, 0, w0, h-3) painter.setBrush(QColor(0,0,0,0)) pen = QPen(Qt.black) pen.setWidth(0) painter.setPen(pen) painter.drawRect(5, 0, w+1, h-3) painter.end() return pix elif role == Qt.ToolTipRole: tot, add, rem = stats msg = "Diff stats:
" msg += " File: %s lines
" % tot msg += " added lines:  %s
" % add msg += " removed lines:  %s" % rem return msg elif column == 0: if role in (Qt.DisplayRole, Qt.ToolTipRole): return tounicode(current_file_desc['desc']) elif role == Qt.DecorationRole: if self._fulllist and ismerge(self.current_ctx): icn = None if current_file_desc['infiles']: icn = geticon('leftright') elif current_file_desc['fromside'] == 'left': icn = geticon('left') elif current_file_desc['fromside'] == 'right': icn = geticon('right') if icn: return icn.pixmap(20,20) elif role == Qt.FontRole: if self._fulllist and current_file_desc['infiles']: font = QFont() font.setBold(True) return font elif role == Qt.ForegroundRole: color = self._flagcolor.get(current_file_desc['flag'], 'black') if color is not None: return QColor(color) return None def headerData(self, section, orientation, role): if ismerge(self.current_ctx): if self._fulllist: header = ('File (all)', 'Diff') else: header = ('File (merged only)', 'Diff') else: header = ('File', 'Diff') if orientation == Qt.Horizontal and role == Qt.DisplayRole: return header[section] return None class TreeItem(object): def __init__(self, data, parent=None): self.parentItem = parent self.itemData = data self.childItems = [] def appendChild(self, item): self.childItems.append(item) return item addChild = appendChild def child(self, row): return self.childItems[row] def childCount(self): return len(self.childItems) def columnCount(self): return len(self.itemData) def data(self, column): return self.itemData[column] def parent(self): return self.parentItem def row(self): if self.parentItem: return self.parentItem.childItems.index(self) return 0 def __getitem__(self, idx): return self.childItems[idx] def __len__(self): return len(self.childItems) def __iter__(self): for ch in self.childItems: yield ch class ManifestModel(QAbstractItemModel): """ Qt model to display a hg manifest, ie. the tree of files at a given revision. To be used with a QTreeView. """ def __init__(self, repo, rev, parent=None): QAbstractItemModel.__init__(self, parent) self.repo = repo self.changectx = self.repo.changectx(rev) self.setupModelData() def data(self, index, role): if not index.isValid(): return None if role != Qt.DisplayRole: return None item = index.internalPointer() return tounicode(item.data(index.column())) def flags(self, index): if not index.isValid(): return Qt.ItemIsEnabled return Qt.ItemIsEnabled | Qt.ItemIsSelectable def headerData(self, section, orientation, role): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self.rootItem.data(section) return None def index(self, row, column, parent): if row < 0 or column < 0 or row >= self.rowCount(parent) or column >= self.columnCount(parent): return QModelIndex() if not parent.isValid(): parentItem = self.rootItem else: parentItem = parent.internalPointer() childItem = parentItem.child(row) if childItem is not None: 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 == self.rootItem: return QModelIndex() return self.createIndex(parentItem.row(), 0, parentItem) def rowCount(self, parent): if parent.column() > 0: return 0 if not parent.isValid(): parentItem = self.rootItem else: parentItem = parent.internalPointer() return parentItem.childCount() def columnCount(self, parent): if parent.isValid(): return parent.internalPointer().columnCount() else: return self.rootItem.columnCount() def setupModelData(self): if self.changectx.rev() is not None: rootData = ["rev %s:%s" % (self.changectx.rev(), short_hex(self.changectx.node()))] else: rootData = ['Working Directory'] self.rootItem = TreeItem(rootData) for path in sorted(self.changectx.manifest()): path = path.split(osp.sep) node = self.rootItem for p in path: for ch in node: if ch.data(0) == p: node = ch break else: node = node.addChild(TreeItem([p], node)) def pathFromIndex(self, index): idxs = [] while index.isValid(): idxs.insert(0, index) index = self.parent(index) return osp.sep.join([index.internalPointer().data(0) for index in idxs]) hgview-1.9.0/hgviewlib/qt4/hgmanifestdialog.py0000644000015700001640000000760012607505500022152 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ Qt4 dialogs to display hg revisions of a file """ import sys, os import os.path as osp from mercurial import ui, hg, util from mercurial.revlog import LookupError from PyQt4 import QtGui, QtCore, Qsci from PyQt4.QtCore import Qt from hgviewlib.application import ManifestViewer as _ManifestViewer from hgviewlib.util import tounicode from hgviewlib.qt4 import icon as geticon from hgviewlib.qt4.mixins import HgDialogMixin, ActionsMixin, ui2cls from hgviewlib.qt4.hgrepomodel import ManifestModel from hgviewlib.qt4.widgets import SourceViewer class ManifestViewer(ActionsMixin, HgDialogMixin, ui2cls('manifestviewer.ui'), QtGui.QMainWindow, _ManifestViewer): """ Qt4 dialog to display all files of a repo at a given revision """ def __init__(self, repo, noderev): self.repo = repo super(ManifestViewer, self).__init__() self.load_ui() self.load_config(repo) self.setWindowTitle('hgview manifest: %s revision %s' % (repo.root, noderev)) # hg repo self.rev = noderev self.setupModels() self.createActions() self.setupTextview() def load_config(self, repo): super(ManifestViewer, self).load_config(repo) self.max_file_size = self.cfg.getMaxFileSize() def setupModels(self): self.treemodel = ManifestModel(self.repo, self.rev) self.treeView.setModel(self.treemodel) self.treeView.selectionModel().currentChanged.connect( self.fileSelected) def createActions(self): # XXX to factorize self.add_action( 'close', self.actionClose, icon='quit', callback=self.close, ) def setupTextview(self): lay = QtGui.QHBoxLayout(self.mainFrame) lay.setSpacing(0) lay.setContentsMargins(0,0,0,0) self.textView = SourceViewer(self.mainFrame) self.setFont(self._font) lay.addWidget(self.textView) def fileSelected(self, index, *args): if not index.isValid(): return path = self.treemodel.pathFromIndex(index) try: fc = self.repo.changectx(self.rev).filectx(path) except LookupError: # may occur when a directory is selected self.textView.setMarginWidth(1, '00') self.textView.setText('') return if fc.size() > self.max_file_size: data = u"file too big" else: # return the whole file data = fc.data() if util.binary(data): data = u"binary file" else: data = tounicode(data) self.textView.set_text(path, data, flag='+', cfg=self.cfg) def setCurrentFile(self, filename): index = QtCore.QModelIndex() path = filename.split(osp.sep) for p in path: self.treeView.expand(index) for row in range(self.treemodel.rowCount(index)): newindex = self.treemodel.index(row, 0, index) if newindex.internalPointer().data(0) == p: index = newindex break self.treeView.setCurrentIndex(index) hgview-1.9.0/hgviewlib/qt4/quickbar.py0000644000015700001640000002643112607505500020451 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ Qt4 QToolBar-based class for quick bars XXX """ from functools import partial from mercurial import util from PyQt4 import QtCore, QtGui from PyQt4.QtCore import pyqtSignal from hgviewlib.util import tounicode from hgviewlib.qt4.mixins import ActionsMixin from hgviewlib.qt4 import icon as geticon Qt = QtCore.Qt class QuickBar(QtGui.QToolBar, ActionsMixin): esc_shortcut_disabled = pyqtSignal(bool) unhidden = pyqtSignal() def __init__(self, parent=None, name='Absctract'): # used to remember who had the focus before bar steel it self.name = name self._focusw = None QtGui.QToolBar.__init__(self, self.name, parent) ActionsMixin.__init__(self) self.setIconSize(QtCore.QSize(16,16)) self.setFloatable(False) self.setMovable(False) self.setAllowedAreas(Qt.BottomToolBarArea) self.createActions() self.createContent() if parent: parent = parent.window() if isinstance(parent, QtGui.QMainWindow): parent.addToolBar(Qt.BottomToolBarArea, self) self.setVisible(False) def createActions(self): self.add_action( 'close', self.tr("Close"), icon='close', tip=self.tr("Close toolbar"), callback=self.hide, ) def setVisible(self, visible=True): if visible and not self.isVisible(): self.unhidden.emit() self._focusw = QtGui.QApplication.focusWidget() QtGui.QToolBar.setVisible(self, visible) self.esc_shortcut_disabled[bool].emit(not visible) if not visible and self._focusw: self._focusw.setFocus() self._focusw = None def createContent(self): raise NotImplementedError def hide(self): self.setVisible(False) def unhide(self): self.setVisible(True) def cancel(self): self.hide() class FindQuickBar(QuickBar): to_find = pyqtSignal(str) to_find_next = pyqtSignal(str) canceled = pyqtSignal() message_logged = pyqtSignal(str, int) def __init__(self, parent, name='find'): QuickBar.__init__(self, parent, name) self.currenttext = '' def createActions(self): QuickBar.createActions(self) self.add_action( 'findnext', self.tr("Find next"), icon='forward', keys=QtGui.QKeySequence("Ctrl+N"), tip=self.tr("Search for the next occurence"), callback=self.find, ) self.add_action( 'cancel', self.tr('Cancel'), tip=self.tr("Cancel processing search"), callback=self.cancel ) def find(self, *args): '''Scan the repository metadata and search for occurrences of the text in the entry. :note: do not scan if no text was provided''' text = self.entry.text() if not text: # do not strip() as user may want to find space sequences self.message_logged.emit('Nothing to look for.', 1000) return if text == self.currenttext: self.to_find_next.emit(text) else: self.currenttext = text self.to_find.emit(text) def cancel(self): self.canceled.emit() def setCancelEnabled(self, enabled=True): self.set_action('cancel', enabled=enabled) self.set_action('findnext', enabled=not enabled) def createContent(self): self.compl_model = QtGui.QStringListModel() self.completer = QtGui.QCompleter(self.compl_model, self) self.entry = QtGui.QLineEdit(self) self.entry.setCompleter(self.completer) self.addWidget(self.entry) self.addActions(self.get_actions('findnext', 'cancel')) self.setCancelEnabled(False) self.entry.returnPressed.connect(self.find) self.entry.textEdited[str].connect(self.find) def setVisible(self, visible=True): QuickBar.setVisible(self, visible) if visible: self.entry.setFocus() self.entry.selectAll() def text(self): if self.isVisible() and self.currenttext.strip(): return self.currenttext def __del__(self): # prevent a warning in the console: # QObject::startTimer: QTimer can only be used with threads started with QThread self.entry.setCompleter(None) class FindInGraphlogQuickBar(FindQuickBar): revision_selected = pyqtSignal([], [int]) file_selected = pyqtSignal(str) def __init__(self, parent, name='find'): FindQuickBar.__init__(self, parent, name) self._findinfile_iter = None self._findinlog_iter = None self._findindesc_iter = None self._fileview = None self._headerview = None self._filter_files = None self._mode = 'diff' self.to_find.connect(self.on_find_text_changed) self.to_find_next.connect(self.on_findnext) self.canceled.connect(self.on_cancelsearch) def setFilterFiles(self, files): self._filter_files = files def setModel(self, model): self._model = model def setMode(self, mode): assert mode in ('diff', 'file') self._mode = mode def attachFileView(self, fileview): self._fileview = fileview def attachHeaderView(self, view): self._headerview = view def find_in_graphlog(self, fromrev, fromfile=None): """ Find text in the whole repo from rev 'fromrev', from file 'fromfile' (if given) *excluded* """ text = self.entry.text() graph = self._model.graph idx = graph.index(fromrev) for node in graph[idx:]: rev = node.rev ctx = self._model.repo.changectx(rev) # XXX should be an re search with undecoded chars as '?' if text in tounicode(ctx.description()): yield rev, None files = ctx.files() if self._filter_files: files = [x for x in files if x in self._filter_files] if fromfile is not None and fromfile in files: files = files[files.index(fromfile)+1:] fromfile = None for filename in files: if self._mode == 'diff': flag, data = self._model.graph.filedata(filename, rev) else: data = ctx.filectx(filename).data() if util.binary(data): data = "binary file" if data and text in data: yield rev, filename else: yield None def cancel(self): if self.get_action('cancel').isEnabled(): self.canceled.emit() else: self.hide() def on_cancelsearch(self, *args): self._findinlog_iter = None self.setCancelEnabled(False) self.message_logged.emit('Search cancelled!', 2000) def on_findnext(self): """ callback called by 'Find' quicktoolbar (on findnext signal) """ if self._findindesc_iter is not None: for pos in self._findindesc_iter: # just highlight next found text in fileview # (handled by _findinfile_iter) return # no more found text in currently displayed file self._findindesc_iter = None if self._findinfile_iter is not None: for pos in self._findinfile_iter: # just highlight next found text in descview # (handled by _findindesc_iter) return # no more found text in currently displayed file self._findinfile_iter = None if self._findinlog_iter is None: # start searching in the graphlog from current position rev = self._fileview.rev() filename = self._fileview.filename() self._findinlog_iter = self.find_in_graphlog(rev, filename) self.setCancelEnabled(True) self.find_next_in_log() def find_next_in_log(self, step=0): """ to be called from 'on_find' callback (or recursively). Try to find the next occurrence of searched text (as a 'background' process, so the GUI is not frozen, and as a cancellable task). """ if self._findinlog_iter is None: # when search has been cancelled return for next_find in self._findinlog_iter: if next_find is None: # not yet found, let's animate a bit the GUI if (step % 20) == 0: self.message_logged.emit( 'Searching'+'.'*(step/20), -1) step += 1 QtCore.QTimer.singleShot(0, lambda: self.find_next_in_log(step % 80)) else: self.message_logged.emit('', -1) self.setCancelEnabled(False) rev, filename = next_find if rev is None: self.revision_selected.emit() else: self.revision_selected[int].emit(rev) text = self.entry.text() if filename is None and self._headerview: self._findindesc_iter = self._headerview.searchString(text) self.on_findnext() else: self.file_selected.emit(tounicode(filename)) if self._fileview: self._findinfile_iter = self._fileview.searchString(text) self.on_findnext() return self.message_logged.emit( 'No more matches found in repository', 2000) self.setCancelEnabled(False) self._findinlog_iter = None def on_find_text_changed(self, newtext): """ callback called by 'Find' quicktoolbar (on find signal) """ newtext = newtext self._findinlog_iter = None self._findinfile_iter = None if self._headerview: self._findindesc_iter = self._headerview.searchString(newtext) if self._fileview: self._findinfile_iter = self._fileview.searchString(newtext) if newtext.strip(): if self._findindesc_iter is None and self._findindesc_iter is None: self.message_logged.emit( 'Search string not found in current diff. ' 'Hit "Find next" button to start searching ' 'in the repository', 2000) else: self.on_findnext() hgview-1.9.0/hgviewlib/qt4/config.py0000644000015700001640000000274312607505500020115 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2013 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, see . # # make sure the Qt rc files are converted into python modules, then load them # this must be done BEFORE other hgview qt4 modules are loaded. """This module contains qt4 specific function for hgview configuration.""" from PyQt4.QtGui import QFont def get_font(cfg): """Return a QFont instance initialized using parameters of the hgview configuration ``cfg``""" fontstr = cfg.getFont() fontsize = cfg.getFontSize() font = QFont() try: if not repr(font.fromString(fontstr)): raise Exception font.setPointSize(fontsize) except: print "bad font name '%s'" % fontstr font.setFamily("Monospace") font.setFixedPitch(True) font.setPointSize(10) return font hgview-1.9.0/hgviewlib/qt4/application.py0000644000015700001640000000421612607505500021150 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ Application utilities. """ import sys from hgrepoviewer import FileViewer, FileDiffViewer, HgRepoViewer, ManifestViewer from hgviewlib.application import HgViewApplication from PyQt4 import QtGui, QtCore class HgViewQtApplication(HgViewApplication): """ HgView application using Qt. """ FileViewer = FileViewer FileDiffViewer = FileDiffViewer HgRepoViewer = HgRepoViewer ManifestViewer = ManifestViewer def __init__(self, *args, **kwargs): # Initiate the application settings. # This allows to use the default QSettings constructor. QtCore.QCoreApplication.setOrganizationName("logilab") QtCore.QCoreApplication.setOrganizationDomain("logilab.org") QtCore.QCoreApplication.setApplicationName("hgview") # This import is critical for qt initialization (at least on Mac os X) import hgviewlib.qt4.hgqv_rc # make Ctrl+C works import signal signal.signal(signal.SIGINT, signal.SIG_DFL) app = QtGui.QApplication(sys.argv) from hgviewlib.qt4 import setup_font_substitutions setup_font_substitutions() super(HgViewQtApplication, self).__init__(*args, **kwargs) self.app = app def exec_(self): self.viewer.show() #pylint: disable=E1103 if '--profile' in sys.argv or '--time' in sys.argv: return 0 return self.app.exec_() hgview-1.9.0/hgviewlib/qt4/__init__.py0000644000015700001640000000607512607505500020411 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . # # make sure the Qt rc files are converted into python modules, then load them # this must be done BEFORE other hgqv qt4 modules are loaded. import os import os.path as osp import sys import datetime as dt import sip sip.setapi('QString', 2) sip.setapi('QVariant', 2) sip.setapi('QDate', 2) sip.setapi('QDateTime', 2) sip.setapi('QTextStream', 2) sip.setapi('QTime', 2) sip.setapi('QUrl', 2) def should_rebuild(srcfile, pyfile): if getattr(sys, "frozen", False): # Py2exe specific: module embedded in return False # the executable and have no file return not osp.isfile(pyfile) or osp.isfile(srcfile) and \ osp.getmtime(pyfile) < osp.getmtime(srcfile) # automatically load resource module, creating it on the fly if # required curdir = osp.dirname(__file__) pyfile = osp.join(curdir, "hgqv_rc.py") rcfile = osp.join(curdir, "hgqv.qrc") if should_rebuild(rcfile, pyfile): if os.system('pyrcc4 %s -o %s' % (rcfile, pyfile)): print "ERROR: Cannot convert the resource file '%s' into a python module." % rcfile print "Please check the PyQt 'pyrcc4' tool is installed, or do it by hand running:" print "pyrcc4 %s -o %s" % (rcfile, pyfile) # load icons from resource and store them in a dict, no matter their # extension (.svg or .png) from PyQt4 import QtCore from PyQt4 import QtGui, uic import hgqv_rc _icons = {} def _load_icons(): t = dt.date.today() x = t.month == 12 and t.day in (24,25) d = QtCore.QDir(':/icons') for icn in d.entryList(): name, ext = osp.splitext(str(icn)) if name not in _icons or ext == ".svg": _icons[name] = QtGui.QIcon(':/icons/%s' % icn) if x: for name in _icons: if name.endswith('_x'): _icons[name[:-2]] = _icons[name] def icon(name): """ Return a QIcon for the resource named 'name.(svg|png)' (the given 'name' parameter must *not* provide the extension). """ if not _icons: _load_icons() return _icons.get(name) # dirty hack to please PyQt4 uic import hgfileview sys.modules['hgfileview'] = hgfileview sys.modules['hgqv_rc'] = hgqv_rc def setup_font_substitutions(): # be sure monospace default font for diffs have a decent substitution # on MacOS QtGui.QFont.insertSubstitutions('monospace', ['monaco', 'courier new']) hgview-1.9.0/hgviewlib/qt4/fileviewer.ui0000644000015700001640000000522612607505500020775 0ustar narvalnarval00000000000000 MainWindow 0 0 481 438 hgview filelog 0 33 481 405 2 Qt::Vertical true QAbstractItemView::SingleSelection QAbstractItemView::SelectRows false Qt::NoPen 0 0 481 33 toolBar TopToolBarArea false Close Ctrl+Q Reload Ctrl+R RevisionsTableView QTableView
revisions_table.h
HgFileView QWidget
hgfileview.h
1
hgview-1.9.0/hgviewlib/qt4/widgets.py0000644000015700001640000002634312607505500020320 0ustar narvalnarval00000000000000# Copyright (c) 2013 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, see . """ Generic Qt4 usefull widgets. """ from itertools import imap from PyQt4.QtGui import QTableView, QFontMetrics, QHeaderView, QLineEdit, \ QPalette, QPainter, QSizePolicy, QFrame from PyQt4.QtCore import QTimer, pyqtSignal, Qt from PyQt4.Qsci import QsciScintilla as qsci from hgviewlib.util import tounicode from hgviewlib.qt4 import icon as geticon from hgviewlib.qt4.styleditemdelegate import StyledItemDelegate from hgviewlib.qt4.lexers import get_lexer class StyledTableView(QTableView): """An Abstract QTableView with some Columns rendered with css. """ def __init__(self, parent=None): super(StyledTableView, self).__init__(parent) self.standard_delegate = self.itemDelegate() self.styled_item_delegate = StyledItemDelegate(self) def setModel(self, model): super(StyledTableView, self).setModel(model) self._reset_delegate() model.layoutChanged.connect(self._reset_delegate) def _reset_delegate(self): # Model column layout has changed so we need to move # our column delegate to correct location model = self.model() if not model: return for index in xrange(model.columnCount()): if self.is_styled_column(index): self.setItemDelegateForColumn(index, self.styled_item_delegate) else: self.setItemDelegateForColumn(index, self.standard_delegate) def is_styled_column(self, index): """ Return True if the column at ``index`` is rendered with style. """ raise NotImplementedError() class SmartResizeTableView(QTableView): """A smart header resizable table.""" def __init__(self, parent=None): super(SmartResizeTableView, self).__init__(parent) self._autoresize = True self.horizontalHeader().sectionResized[int, int, int].connect( self.disableAutoResize) def enableAutoResize(self, *args): self._autoresize = True def disableAutoResize(self, *args): self._autoresize = False QTimer.singleShot(100, lambda: self.enableAutoResize()) def resizeEvent(self, event): # we catch this event to resize smartly tables' columns super(SmartResizeTableView, self).resizeEvent(event) if self._autoresize: self.resizeColumns() def resizeColumns(self, *args): # resize columns the smart way: the column holding Log # is resized according to the total widget size. model = self.model() if not model: return col1_width = self.viewport().width() fontm = QFontMetrics(self.font()) tot_stretch = 0.0 for index in range(model.columnCount()): stretch = self.get_column_stretch(index) if stretch is not None: tot_stretch += stretch continue width = model.maxWidthValueForColumn(index) if width is not None: width = fontm.width(tounicode(width) + u'W') self.setColumnWidth(index, width) else: width = self.sizeHintForColumn(index) self.setColumnWidth(index, width) col1_width -= self.columnWidth(index) col1_width = max(col1_width, 100) for index in range(model.columnCount()): stretch = self.get_column_stretch(index) if stretch is not None: width = stretch / tot_stretch self.setColumnWidth(index, col1_width * width) def setModel(self, model): super(SmartResizeTableView, self).setModel(model) col = col = list(model._columns).index('Log') self.horizontalHeader().setResizeMode(col, QHeaderView.Stretch | QHeaderView.Interactive) def get_column_stretch(self, index): raise NotImplementedError class QueryLineEdit(QLineEdit): """Special LineEdit class with visual marks for the revset query status""" text_edited_no_blank = pyqtSignal(str) FORGROUNDS = {'normal':Qt.color1, 'valid':Qt.color1, 'failed':Qt.darkRed, 'query':Qt.darkGray} ICONS = {'valid':'valid', 'query':'loading'} def __init__(self, parent): self._parent = parent self._status = None # one of the keys of self.FORGROUNDS and self.ICONS super(QueryLineEdit, self).__init__(parent) self.setTextMargins(0,0,-16,0) self.textEdited.connect(self.on_text_edited) self.previous_text = '' def set_status(self, status=None): self._status = status color = self.FORGROUNDS.get(status, None) if color is not None: palette = self.palette() palette.setColor(QPalette.Text, color) self.setPalette(palette) def get_status(self): return self._status status = property(get_status, set_status, None, "query status") def paintEvent(self, event): super(QueryLineEdit, self).paintEvent(event) icn = geticon(self.ICONS.get(self._status)) if icn is None: return painter = QPainter(self) icn.paint(painter, self.width() - 18, (self.height() - 18) / 2, 16, 16) def on_text_edited(self): current_text = self.text().strip() if current_text == self.previous_text: return self.previous_text = current_text self.text_edited_no_blank.emit( current_text) class Annotator(qsci): # we use a QScintilla for the annotator cause it makes # it much easier to keep the text area and the annotator sync # (same font rendering etc). However, it have the drawback of making much # more difficult to implement things like QTextBrowser.anchorClicked, which # would have been nice to directly go to the annotated revision... def __init__(self, textarea, parent=None): super(Annotator, self).__init__(parent) self.setFrameStyle(0) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setReadOnly(True) self.sizePolicy().setControlType(QSizePolicy.Slider) self.setMinimumWidth(20) self.setMaximumWidth(40) # XXX TODO make this computed self.setFont(textarea.font()) self.setMarginWidth(0, '') self.setMarginWidth(1, '') self.SendScintilla(qsci.SCI_SETCURSOR, 2) self.SendScintilla(qsci.SCI_SETCARETSTYLE, 0) # used to set a background color for every annotating rev N = 32 self.markers = [] for i in range(N): marker = self.markerDefine(qsci.Background) color = 0x7FFF00 + (i-N/2)*256/N*256*256 - i*256/N*256 + i*256/N self.SendScintilla(qsci.SCI_MARKERSETBACK, marker, color) self.markers.append(marker) textarea.verticalScrollBar().valueChanged[int].connect( self.verticalScrollBar().setValue) def set_line_ticks(self, ticks): """Specify line ticks instead of line numbers. A background color is automatically added. A new background color is used when the tick changes. """ self.setText('\n'.join(ticks)) uniq_ticks = list(sorted(set(ticks))) for i, tick in enumerate(ticks): idx = uniq_ticks.index(tick) self.markerAdd(i, self.markers[idx % len(self.markers)]) class SourceViewer(qsci): def __init__(self, *args, **kwargs): super(SourceViewer, self).__init__(*args, **kwargs) self.setUtf8(True) self.setFrameStyle(0) self.setFrameShape(QFrame.NoFrame) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.setReadOnly(True) self.SendScintilla(qsci.SCI_SETSELEOLFILLED, True) self.SendScintilla(qsci.SCI_SETCARETSTYLE, 0) # margin 1 is used for line numbers self.setMarginLineNumbers(1, True) self.setMarginWidth(1, '000') self.SendScintilla(qsci.SCI_INDICSETSTYLE, 8, qsci.INDIC_ROUNDBOX) self.SendScintilla(qsci.SCI_INDICSETUNDER, 8, True) self.SendScintilla(qsci.SCI_INDICSETFORE, 8, 0xBBFFFF) self.SendScintilla(qsci.SCI_INDICSETSTYLE, 9, qsci.INDIC_ROUNDBOX) self.SendScintilla(qsci.SCI_INDICSETUNDER, 9, True) self.SendScintilla(qsci.SCI_INDICSETFORE, 9, 0x58A8FF) # hide margin 0 (markers) self.SendScintilla(qsci.SCI_SETMARGINTYPEN, 0, 0) self.SendScintilla(qsci.SCI_SETMARGINWIDTHN, 0, 0) # setup margin 1 for line numbers only self.SendScintilla(qsci.SCI_SETMARGINTYPEN, 1, 1) self.SendScintilla(qsci.SCI_SETMARGINWIDTHN, 1, 20) self.SendScintilla(qsci.SCI_SETMARGINMASKN, 1, 0) # define markers for colorize zones of diff self.markerplus = self.markerDefine(qsci.Background) self.SendScintilla(qsci.SCI_MARKERSETBACK, self.markerplus, 0xB0FFA0) self.markerminus = self.markerDefine(qsci.Background) self.SendScintilla(qsci.SCI_MARKERSETBACK, self.markerminus, 0xA0A0FF) self.markertriangle = self.markerDefine(qsci.Background) self.SendScintilla(qsci.SCI_MARKERSETBACK, self.markertriangle, 0xFFA0A0) def clear_highlights(self): n = self.length() self.SendScintilla(qsci.SCI_SETINDICATORCURRENT, 8) # highlight self.SendScintilla(qsci.SCI_INDICATORCLEARRANGE, 0, n) self.SendScintilla(qsci.SCI_SETINDICATORCURRENT, 9) # current found # occurrence self.SendScintilla(qsci.SCI_INDICATORCLEARRANGE, 0, n) def highlight_current_search_string(self, pos, text): line = self.SendScintilla(qsci.SCI_LINEFROMPOSITION, pos) self.ensureLineVisible(line) self.SendScintilla(qsci.SCI_SETINDICATORCURRENT, 9) self.SendScintilla(qsci.SCI_INDICATORCLEARRANGE, 0, pos) self.SendScintilla(qsci.SCI_INDICATORFILLRANGE, pos, len(text)) def search_and_highlight_string(self, text): data = unicode(self.text()) self.SendScintilla(qsci.SCI_SETINDICATORCURRENT, 8) pos = [data.find(text)] n = len(text) while pos[-1] > -1: self.SendScintilla(qsci.SCI_INDICATORFILLRANGE, pos[-1], n) pos.append(data.find(text, pos[-1]+1)) return [x for x in pos if x > -1] def set_text(self, filename, data, flag=None, cfg=None): lexer = get_lexer(filename, data, flag, cfg) if lexer: # lexer.setFont(self.font()) self.setLexer(lexer) nlines = data.count('\n') self.setMarginWidth(1, str(nlines)+'00') self.setText(data) hgview-1.9.0/hgviewlib/qt4/resources/0000775000015700001640000000000012607506603020311 5ustar narvalnarval00000000000000hgview-1.9.0/hgviewlib/qt4/resources/description.css0000644000015700001640000001365712607505500023353 0ustar narvalnarval00000000000000/* :Author: David Goodger (goodger@python.org) :Id: $Id: html4css1.css 7056 2011-06-17 10:50:48Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to customize this style sheet. */ /* used to remove borders from tables and images */ .borderless, table.borderless td, table.borderless th { border: 0 } table.borderless td, table.borderless th { /* Override padding for "table.docutils td" with "! important". The right padding separates the table cells. */ padding: 0 0.5em 0 0 ! important } .first { /* Override more specific margin styles with "! important". */ margin-top: 0 ! important } .last, .with-subtitle { margin-bottom: 0 ! important } .hidden { display: none } a.toc-backref { text-decoration: none ; color: black } blockquote.epigraph { margin: 2em 5em ; } dl.docutils dd { margin-bottom: 0.5em } object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] { overflow: hidden; } /* Uncomment (and remove this text!) to get bold-faced definition list terms dl.docutils dt { font-weight: bold } */ div.abstract { margin: 2em 5em } div.abstract p.topic-title { font-weight: bold ; text-align: center } div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning { margin: 2em ; border: medium outset ; padding: 1em } div.admonition p.admonition-title, div.hint p.admonition-title, div.important p.admonition-title, div.note p.admonition-title, div.tip p.admonition-title { font-weight: bold ; font-family: sans-serif } div.attention p.admonition-title, div.caution p.admonition-title, div.danger p.admonition-title, div.error p.admonition-title, div.warning p.admonition-title { color: red ; font-weight: bold ; font-family: sans-serif } /* Uncomment (and remove this text!) to get reduced vertical space in compound paragraphs. div.compound .compound-first, div.compound .compound-middle { margin-bottom: 0.5em } div.compound .compound-last, div.compound .compound-middle { margin-top: 0.5em } */ div.dedication { margin: 2em 5em ; text-align: center ; font-style: italic } div.dedication p.topic-title { font-weight: bold ; font-style: normal } div.figure { margin-left: 2em ; margin-right: 2em } div.footer, div.header { clear: both; font-size: smaller } div.line-block { display: block ; margin-top: 1em ; margin-bottom: 1em } div.line-block div.line-block { margin-top: 0 ; margin-bottom: 0 ; margin-left: 1.5em } div.sidebar { margin: 0 0 0.5em 1em ; border: medium outset ; padding: 1em ; background-color: #ffffee ; width: 40% ; float: right ; clear: right } div.sidebar p.rubric { font-family: sans-serif ; font-size: medium } div.system-messages { margin: 5em } div.system-messages h1 { color: red } div.system-message { border: medium outset ; padding: 1em } div.system-message p.system-message-title { color: red ; font-weight: bold } div.topic { margin: 2em } h1.section-subtitle, h2.section-subtitle, h3.section-subtitle, h4.section-subtitle, h5.section-subtitle, h6.section-subtitle { margin-top: 0.4em } h1.title { text-align: center } h2.subtitle { text-align: center } hr.docutils { width: 75% } img.align-left, .figure.align-left, object.align-left { clear: left ; float: left ; margin-right: 1em } img.align-right, .figure.align-right, object.align-right { clear: right ; float: right ; margin-left: 1em } img.align-center, .figure.align-center, object.align-center { display: block; margin-left: auto; margin-right: auto; } .align-left { text-align: left } .align-center { clear: both ; text-align: center } .align-right { text-align: right } /* reset inner alignment in figures */ div.align-right { text-align: inherit } /* div.align-center * { */ /* text-align: left } */ ol.simple, ul.simple { margin-bottom: 1em } ol.arabic { list-style: decimal } ol.loweralpha { list-style: lower-alpha } ol.upperalpha { list-style: upper-alpha } ol.lowerroman { list-style: lower-roman } ol.upperroman { list-style: upper-roman } p.attribution { text-align: right ; margin-left: 50% } p.caption { font-style: italic } p.credits { font-style: italic ; font-size: smaller } p.label { white-space: nowrap } p.rubric { font-weight: bold ; font-size: larger ; color: maroon ; text-align: center } p.sidebar-title { font-family: sans-serif ; font-weight: bold ; font-size: larger } p.sidebar-subtitle { font-family: sans-serif ; font-weight: bold } p.topic-title { font-weight: bold } pre.address { margin-bottom: 0 ; margin-top: 0 ; font: inherit } pre.literal-block, pre.doctest-block, pre.math { margin-left: 2em ; margin-right: 2em } span.classifier { font-family: sans-serif ; font-style: oblique } span.classifier-delimiter { font-family: sans-serif ; font-weight: bold } span.interpreted { font-family: sans-serif } span.option { white-space: nowrap } span.pre { white-space: pre } span.problematic { color: red } span.section-subtitle { /* font-size relative to parent (h1..h6 element) */ font-size: 80% } table.citation { border-left: solid 1px gray; margin-left: 1px } table.docinfo { margin: 2em 4em } table.docutils { margin-top: 0.5em ; margin-bottom: 0.5em } table.footnote { border-left: solid 1px black; margin-left: 1px } table.docutils td, table.docutils th, table.docinfo td, table.docinfo th { padding-left: 0.5em ; padding-right: 0.5em ; vertical-align: top } table.docutils th.field-name, table.docinfo th.docinfo-name { font-weight: bold ; text-align: left ; white-space: nowrap ; padding-left: 0 } h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { font-size: 100% } ul.auto-toc { list-style-type: none } hgview-1.9.0/hgviewlib/qt4/revision_description.py0000644000015700001640000002505612607505500023113 0ustar narvalnarval00000000000000# Copyright (c) 2009-2012 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, see . """ Qt4 high level widgets for hg repo changelogs and filelogs """ from collections import defaultdict from mercurial.node import short as short_hex from mercurial.error import RepoError from PyQt4 import QtCore, QtGui from PyQt4.QtCore import Qt, pyqtSignal from hgviewlib.config import HgConfig from hgviewlib.util import format_desc, xml_escape, tounicode from hgviewlib.util import first_known_precursors, first_known_successors from hgviewlib.qt4.mixins import ActionsMixin # Re-Structured Text support raw2html = lambda x: u'
%s
' % xml_escape(x) try: from docutils.core import publish_string import docutils.utils def rst2html(text, stylesheet_path=None): if stylesheet_path is None: stylesheet_path = 'qrc:/resources/description.css' try: # halt_level allows the parser to raise errors # report_level cleans the standard output out = publish_string(text, writer_name='html', settings_overrides={ 'halt_level':docutils.utils.Reporter.WARNING_LEVEL, 'report_level':docutils.utils.Reporter.SEVERE_LEVEL + 1, 'stylesheet_path': stylesheet_path, 'embed_stylesheet': False, }) except: # docutils is not always reliable (or reliably packaged) out = raw2html(text) if not isinstance(out, unicode): # if the docutils call did not fail, we likely got an str ... out = tounicode(out) return out except ImportError: rst2html = None TROUBLE_EXPLANATIONS = defaultdict(lambda:u'unknown trouble') TROUBLE_EXPLANATIONS['unstable'] = "Based on obsolete ancestor" TROUBLE_EXPLANATIONS['bumped'] = "Hopeless successors of a public changeset" TROUBLE_EXPLANATIONS['divergent'] = "Another changeset are also a successors "\ "of one of your precursor" # temporary compat with older evolve version TROUBLE_EXPLANATIONS['latecomer'] = TROUBLE_EXPLANATIONS['bumped'] TROUBLE_EXPLANATIONS['conflicting'] = TROUBLE_EXPLANATIONS['divergent'] class RevisionDescriptionView(ActionsMixin, QtGui.QTextBrowser): """ Display metadata for one revision (rev, author, description, etc.) using a TextBrowser. """ parent_revision_selected = pyqtSignal([], [int]) revision_selected = pyqtSignal([], [int]) def __init__(self, parent=None): super(RevisionDescriptionView, self).__init__(parent) self.excluded = () self.descwidth = 60 # number of chars displayed for parent/child descriptions self.add_action( 'rst', self.tr("Fancy description"), menu=self.tr("Display"), tip=self.tr('Interpret ReST comments'), callback=self.refreshDisplay, checked=bool(rst2html), enabled=bool(rst2html), ) self.anchorClicked.connect(self.on_anchor_clicked) def on_anchor_clicked(self, qurl): """ Callback called when a link is clicked in the text browser """ rev = qurl.toString() diff = False if rev.startswith('diff_'): rev = int(rev[5:]) diff = True try: rev = self.ctx._repo.changectx(rev).rev() except RepoError: QtGui.QDesktopServices.openUrl(qurl) self.refreshDisplay() if diff: self.diffrev = rev self.refreshDisplay() # TODO: emit a signal to recompute the diff if self.diffrev is None: self.parent_revision_selected.emit() else: self.parent_revision_selected[int].emit(self.diffrev) else: if rev is None: self.revision_selected.emit() else: self.revision_selected[int].emit(rev) def setDiffRevision(self, rev): if rev != self.diffrev: self.diffrev = rev self.refreshDisplay() def displayRevision(self, ctx): self.ctx = ctx self.diffrev = ctx.parents()[0].rev() if hasattr(self.ctx._repo, "mq"): self.mqseries = self.ctx._repo.mq.series[:] self.mqunapplied = [x[1] for x in self.ctx._repo.mq.unapplied(self.ctx._repo)] mqpatch = set(self.ctx.tags()).intersection(self.mqseries) if mqpatch: self.mqpatch = mqpatch.pop() else: self.mqpatch = None else: self.mqseries = [] self.mqunapplied = [] self.mqpatch = None self.refreshDisplay() def selectNone(self): cursor = self.textCursor() cursor.clearSelection() cursor.setPosition(0) self.setTextCursor(cursor) self.setExtraSelections([]) def searchString(self, text): self.selectNone() if text in self.toPlainText(): clist = [] while self.find(text): eselect = self.ExtraSelection() eselect.cursor = self.textCursor() eselect.format.setBackground(QtGui.QColor('#ffffbb')) clist.append(eselect) self.selectNone() self.setExtraSelections(clist) def finditer(self, text): if text: while True: if self.find(text): yield self.ctx.rev(), None else: break return finditer(self, text) def refreshDisplay(self): ctx = self.ctx rev = ctx.rev() cfg = HgConfig(ctx._repo.ui) buf = u"\n" if self.mqpatch: buf += u'' % cfg.getMQFGColor() buf += u'\n' buf += u'' if rev is None: buf += u"\n" else: buf += u'\n' % (ctx.rev(), short_hex(ctx.node())) user = tounicode(ctx.user()) if ctx.node() else u'' buf += '\n' % user buf += '\n' % tounicode(ctx.branch()) buf += '\n' % tounicode(ctx.phasestr()) buf += '' buf += "
Patch queue: ' for p in self.mqseries: if p in self.mqunapplied: p = u"%s" % tounicode(p) elif p == self.mqpatch: p = u"%s" % tounicode(p) buf += u' %s ' % tounicode(p) buf += u'
Working Directory'\ u'%s:'\ u'%s'\ u'%s%s%s
\n" buf += "\n" parents = [p for p in ctx.parents() if p] for p in parents: if p.rev() > -1: buf += self._html_ctx_info(p, 'Parent', 'Direct ancestor of this changeset') if len(parents) == 2: p = parents[0].ancestor(parents[1]) buf += self._html_ctx_info(p, 'Ancestor', 'Direct ancestor of this changeset') for p in ctx.children(): r = p.rev() if r > -1 and r not in self.excluded: buf += self._html_ctx_info(p, 'Child', 'Direct descendant of this changeset') for prec in first_known_precursors(ctx, self.excluded): buf += self._html_ctx_info(prec, 'Precursor', 'Previous version obsolete by this changeset') for suc in first_known_successors(ctx, self.excluded): buf += self._html_ctx_info(suc, 'Successors', 'Updated version that make this changeset obsolete') bookmarks = ', '.join(tounicode(bookmark) for bookmark in ctx.bookmarks()) if bookmarks: buf += ''\ ''\ '\n' % bookmarks troubles = ctx.troubles() if troubles: span = u'%s' content = u', '.join([span % (TROUBLE_EXPLANATIONS[troub], troub) for troub in troubles]) buf += ''\ ''\ '\n' % ''.join(content) buf += u"
Bookmarks: '\ '%s
Troubles: '\ '%s
\n" desc = tounicode(ctx.description()) if self.get_action('rst').isChecked(): replace = cfg.getFancyReplace() if replace: desc = replace(desc) desc = rst2html(desc, cfg.getDescriptionStylePath()) else: desc = raw2html(desc) buf += u'
%s
\n' % desc self.setHtml(buf) def _html_ctx_info(self, ctx, title, tooltip=None): isdiffrev = ctx.rev() == self.diffrev if not tooltip: tooltip = title short = short_hex(ctx.node()) if getattr(ctx, 'applied', True) else ctx.node() descr = format_desc(ctx.description(), self.descwidth) rev = ctx.rev() out = ''\ '%(title)s:'\ '' % locals() if isdiffrev: out += '' out += ''\ '%(rev)s'\ ':'\ '%(short)s '\ '%(descr)s' % locals() if isdiffrev: out += '' out += '\n' return out hgview-1.9.0/hgviewlib/qt4/icons/0000775000015700001640000000000012607506603017412 5ustar narvalnarval00000000000000hgview-1.9.0/hgviewlib/qt4/icons/diffmode.png0000755000015700001640000000304612607505500021674 0ustar narvalnarval00000000000000PNG  IHDR szztEXtSoftwareAdobe ImageReadyqe<IDATxW[le>nKWLUTQʃ$ tK^%Q1ҥKV@D$\S.yʋEݝfvgfgfw b8o{`iG*S( Ȥ{ 2j&@JҚ?C5q`%)!.C<#"vQ>l@vIn*3ړ'3<ᛁP<Gia~LC$i& Ev+?G=\-} LU{.gZ(8UmF"{:aC%r`aKS;&%k@[s[UxuE@a(@4!Ŋ  QAex6'X̚$N& .xoPM۾r1PeXSDlt!+PoY"lh6 ˁx!ʠ|.Ozd^t= 1pw~RJt!zJ΋a=C&6wVmnWava^V4[ZLwL B5\Jpڃ#5$."ts{V{$Kb;uyu-н*ނOf$pװqvMp0d%gP,;]/]SBu@(`Γ&s9ӅOv 7pD$C#>e"tledy<-]Ce1yߗFW~d˛xzii(ˋM"Ͱ`}aIIA{BFxHS@~ Dd!#fP2ŧ {qG1Z>ʼn0E&oŊwR<^8Ճ&]64+e1r/*}Kk s?@9V }3@`*H(JT }?Mz+>2dBza""{X' tgVp,"C%ͤB{f6mFoIENDB`hgview-1.9.0/hgviewlib/qt4/icons/mqdiff.svg0000644000015700001640000003123212607505500021373 0ustar narvalnarval00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Up go higher up arrow pointer > Andreas Nilsson ! hgview-1.9.0/hgviewlib/qt4/icons/find.svg0000644000015700001640000003661412607505500021056 0ustar narvalnarval00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz hgview-1.9.0/hgviewlib/qt4/icons/showhide.png0000755000015700001640000000120312607505500021722 0ustar narvalnarval00000000000000PNG  IHDRw=bKGD pHYs  ~tIME +$#YIDATx͕?A3QDD )"ҥBi٤IiIicH!`"1OS_Ɂ.޹gν;c/Aޠ 7@~_R('4C{8c ]"U#@n|zia/t_7 2 R&-bXMt&KӁ2uRJ)Bh4"~r8R)%B*x~d2,Z0 b1Az)fBy&Q^4!VbT*^S|ťضM{_(FzdjH)Ra\y:zx4}_ѠRL$բ@)%gggcn9>H$j8zm˲g$io^`&<>yE l~^w=@o]p H\[Kvb|*!{Sgݫ%}IENDB`hgview-1.9.0/hgviewlib/qt4/icons/unfilter.png0000644000015700001640000000442112607505500021742 0ustar narvalnarval00000000000000PNG  IHDR szzsRGBbKGD pHYs  tIME 'glVIDATXåk]U>g3sP>)T0* h T1#`(JRFLР1jmyV@RJN;әvs޹w>SRZu%{k#|\u.d2sәtK:n&eRJXY)B|?=׫k_\8nVnʶ'_mڰͻZ?K.tK:n5M3,%RX s=<#biJ%0MJ~T)wuvWzzjʮk/~7/?)xZb䚛dʥ MYMY247eH ݻo˼q¹$-T*U:w;ro0?}>..jͫ&̞;l&R&0R 0;`bTCkJ%n{{<qw|#n8P,ioRJY(eb)u?ҒZ&b R:fpsg/] zZ#Lkoe_ c8"ģ5ZkLaG1q1L0<ui6WDO..t!RxoHiRX kbNkMtvgAKK g`9m!BBaB@qH~t(B۶q])xlxM AJ\!04 4!PJL%PJa愫L4,E2$Jfٵ{|BZ͟+N~oVtpEP PXJ!@kM0$J,TB) a*QH@{>O|sP|u{oF=ھaJ%Ǎ'4gLёkC)C xHH~hDaalsgzvoyCO@ilW=w?;{ss6 0,,JDQyaHFDTk :~pZut*528xd.@P/z}C?ywpp~REFLN;]15隘rid"IZYٵqm?uO{k|q0th` ! R` zh[gNT &ҩԵ[}C; ibf#$Bn1@0$"̣cA0V4̊?:YiȵgMTT+3?t9J}e׮CCuZɤHX 0D0x(e" E|}k=2o/spժţƾϬO?[=7V,cHXkRTS)(BLzNatDZ_m4T@&\5ϜE {[2=k(4IDфXcD>>Nq_7=Mh0^$א+oXrGXRko#n0<ĩvBkDnP*dr 266ئM[:d28jxE?߿M={ۧc1IENDB`hgview-1.9.0/hgviewlib/qt4/icons/leftright.svg0000644000015700001640000001015712607505500022120 0ustar narvalnarval00000000000000 image/svg+xml hgview-1.9.0/hgviewlib/qt4/icons/clean.svg0000644000015700001640000012700412607505500021212 0ustar narvalnarval00000000000000 image/svg+xml weather-clear January 2006 Ryan Collier (pseudo) http://www.tango-project.org http://www.pseudocode.org weather applet notification Garrett LeSage hgview-1.9.0/hgviewlib/qt4/icons/forward.svg0000644000015700001640000001731412607505500021576 0ustar narvalnarval00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Next go next right arrow pointer > hgview-1.9.0/hgviewlib/qt4/icons/down.svg0000644000015700001640000002015412607505500021075 0ustar narvalnarval00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Down go lower down arrow pointer > Andreas Nilsson hgview-1.9.0/hgviewlib/qt4/icons/heavy.png0000644000015700001640000004465112607505500021237 0ustar narvalnarval00000000000000PNG  IHDR>agAMA7 IDATxy|ս̒YMv2$;@JRnz[{{Vmo* цE(.IIId_&}}~YɆ '=T*.EAF'v4%ܺ|A韉T}f叕d0[JevNe?VxjGh(iѨx$7 7";\MKe|N#q$a\31`>`C1Y7kO~ن{/]t?YFV0KRlĵ"A#n3f+!fϋ=.a8T{௽c?;6%ܨ7SEA@6``;5 ^k6&O]֣]Elk؏\"OƋ.O?9Y[Ά xO[?|5{Eb0kw#B R5%# רl)rgZXs e ?)b1io> lZy۟|Q9 .'jҹk&^T^( xoqJz5wRWis Š@~eCԽ[h\M}잛\=)WIyw/D)aoOV1?ػ~*6Ghͥ=mBWPQ\ƎY*SIseQվr6|?L'@VH1}cW~?Gٽ6Ț;H4NC_rꤨo"( Є1G$34ʯWc]/q !- ޵wL7}\ϾN6m77+e ~%ϬtD`%5qҟHuZJ/r<~Cβ;f03N-X3}X+Q{0斑? P#00` IX-T4X\.Ҩa1Ɣ;fd dxWq٭%X5MS^~w.Àt`m(5ÿ|.9Ƥ-Z_en^76OhȲ5F9~,p,2u\6-- t#pk j|~~jZxv/5-u9ǡ(D%4&[Sqࠡ76 `knĚ[d $%2AHwkRā6KnԼҲX_ />MKyzښ WNk<5z?7G[J}N%(l@Wr ֠kykai!-!3No8!#7n8cle-5󱨵59XJXs'LDURJ~Iʸy*I m9U荩-ccǘ-a#wL˩c̚ݼ!:S\i.(/7k?>'JC͸"a.\R\Z~ t3A"+Xba4ݭ.IoR @>q}if#Ut80\\S_OA̙\x)gGvv6Pxcmmq 3zݜ=u/wrj9_JjMC4sm@>Ν%i%{P  5>2ĨaQvyG˼j&Y-+Nz[`Rw9x7GC,jkcuc#) nn ,;>B/rKA}.9v<(~TUk2(O1;o_Bc=`~kmyTl.I1̿cu-lKt /=7a>\)/M=vPH(¶eWӒpS%i<RܚϏ:=f'\lwx;_!SMyFiw|܃n`9V?3hs ϳ;ppn^?hykAÌs>sf"ANO7`a 06G ֶLE^'7]Ow.b栌X/tH˟BG5oܽs }L-=͇U(8a㿯^\} t i /qo}=+YqI~ dffoDVu$l!c* PQ/1#Wp\7 6-JE(F qN~mF)HJ%ꄨ,o= U1)#1RQ:y)PB/>"a@}n=,[q)۷B/& rHtTf*CZ|s9&fY{X-~Y<^ lk-YPf Ja i賊z]</!W,## IRCc2* <TG$Zζ,ũeX_` iP(HӪ<@EX/V~3Qê?sGKm3پ Jr'qI9^& \U< JbR<ĜͲɭI+Ҫwdpt8tSXOK] oB,QȢt( )Am@AIEIAPF}FH3 =:%v[=oZIΈ_EISz 7wHѪzX ii9%5U$H`$ 2*JT,HzMԙҰZÙ"RWCOkSB_<$n6v?_%Ź|bQ E*ͼkܛp,;w2}搕[9snA4>B6Ξ:rs򊫱MyD-ۆm'px Ԟ¶C82Dt/ PTʯTuJ,صI(:$!b,ۨݷjH*W#XlLtڔq/ɞ}Q`9<BAQ׏ߗqCͥ;(A0ǎL|Pi@' oB*%K6Rt[+Sh^Е=nc,\pkVXY8kp>Cʯ :1IZqtGA0@>/Ik{'}nc2\0@DҧeU"7SVVRr E-u5lt-u5=C͛n~kV~h[0K{֝;v10)Qx pۣ?BRͫUxeFbܠQdn.(ƺRHhY";|'[S^zNfųѦoH,?| ċk`oe_Bcҏ_ABm:1~#~^~nN*fp mrFɏ_Hu庿+胩d5tO߰gܷopٔMC(;~iV *l-~GdΚ:6?X#AcDO^=)_lQa/@(_J r#'z}|Zc,aԖI0saMb'7=hS4g gߑCAPs\|׿[[KÜ݋dM` nbcǡM$xN~0*V.N^C8>>$h.@t^0؄b_n:6tfd-_BU/x|~2R CMC=u6҅+YD~__]rnA-m_< Z1$E`*{<^^'K4'GwPIdTd yWh| yKv(JBq% |z=5oN׿瞃Il))Y첕(S{Tsn@0R*پ3"W_oL?|}^Qj&9ƹ7ާ%(Մ?[{ <8Er( 0y yxC+OFd@ZMT/D.FU<֣ÕA֜i\#8'A~>@c#⸤χyț;/lgoޏ~:cJ<7L!weڵdD#;7n܇Ɛr0@((E7؊c_8|M侜mhI~/"L(Q)Yn.(>:t$ˠ{4CUN͹T=!yxC8/cT)gsMk0F1* Df/%_.5^8r $ ݊.[F㫯hCLY]B  }4|3;^7-(7c44RvXhtQIP^kEߎhJOMZ͒|&mm8@ɓKK[LxUx j'i2u[B彏s##hJ04 ߶yhAtGG(2B" `x>qe4rr`$aZ[1U*&O$|AWAqo}K~S4&ɔ!j>#ߌԇ1'$"=v悢0Cpx_`f@ ou~ 0 @o/?x>%%H`6&Y1;GW XsH b6ߎ:gpR ķюݣ:j Iaj9}UF93A& "%R9gùs䕖Eߩv^W#178n! \= 8#?צ69(Vk3^שÇYQ!l,Ъ^x :jn%E4 𹐃cV]_;aCC@8r`)!TA|@ O iB!ҥBZ yw(؃ TQdY giɡNcĨ0 ! FzAa$>>2e MfY D G 0 "Ba^hhS #]]]~#T}F +<ج|-%cI#I|)pCҦ;=k3C6Kf|y%̄ؐpIR~ 9sD @0ͻB}1y$ßz=*(;ߪV!\زRߍE! 25P$r8)OCB( FP3B2xcL̕CsQ0|D{Z[gfªU0o؏Д)"P[ #tu +SeZI OS1z5W$xl|㍭Q=TF9?.pvACfX5pK`t[/C%|8Gc~ KhUu`\~hRDee47þ}>뒲2Lj5GOstT2}l2gdGhb{=$~~4"~{+j zr9I}Q;Gͷ?@׭NjeᢋK/g?+N8|XkjZ,Xs3GC0f4.vUu:Iz71Oݼc +1:{C,VgXe|LPtk{/V9KDk>'GV*}89;&٨VHVdB!`L8{6}d<9 oO 6X\ !yNPJ~ίbw p̳šGhaꞞI&O$;uhGԚL¶gdPm6!Á?u >jjizv:qI.@3I'|ޓ] 1ps4=h &KE@,3B߿hZ#sGf qDdi{wy*uitn׈H|ڃ0<_APTCMKG7ݢ L$E" Fަs]uۘiWsCBrGM,h`GԌ&B->-zq./pEQB.eȎ"n3탣|x%A1=9NCB|A!{+iHJ5N-!rZ8>~ xBY9R( pVE1sSUc(YPMG8{GsČ`JaUU|0(AݢHP-˄AP|t/6P`JNtMW~ qQ8 Fh1g/%0 xO)1eنL<`I܇^ [U! xZ׳d>WˣhH7 E,Id3@5Ā70mMS,ERD~3?Ľ' ΔHBȬQ/W䐈QzvB>׮\Dq:vn2ii*KѽuH܏oCB!-L11qc9֔!C3hQU[^䵥ܰLj{fP4:)dhGېb욳߃,QK~V纰9q)̻`p;!g%BٷfPjK"r5+hbp +*[kMg=c_U}cx*.ԒbiMg=l`>)E(CQ Ԏ0pC* kxwh:;9;-ypiRϠ}nG Lt2֤7 PǼUsԁm 06|nx>屡X5I"&BŅZ6MϾFǭw"> HD zjer6DУ3%+m?f${|LNO'J LEVXQPfָn=+a}:|#İ]]}/gƏ'_atצz}*<{D n%@%8f6'D#4%V[j 3%6\g|S ;b%sJ$ {ec\ C̟PY9%vzts|>OM=mi--5= XhDiO zo_ {&" ()ؾ I5ַ/%5UEIԡVw\gnn HV ROwa9XqLai_#Ju,W @c۸wr3_obLxTp%f*0 D%ߍ^ R0|+.[ϝߛ 3svޯsJ$ZO@cM (UX4po.|*3 Cx|ⶻGmu\S${ኛ1tOR~@hI#p |x@~6T,,|U/~b4"&VAOCn2JWi$%hRXcbcӥ`ۑ>Ķ]]\yU\zbS'F;eշ|cܚ!A ]IVxpIv0 ؃o}fGfSlVgڍMy{Ǯn+wOc.^~I6W;SͶ *w ɂ=^%qIn*NJ'WB\2(|kJ{IEڻN".PTDby]v۟DsX}˝CΎ *Ag {) r#Gd?!KdSYPOR7<]Tm­YQv/1*M4H*[+`Pq4%(` (]2)28dWn2w;W%KMfcq9l%>Ą׏|q_֑勏MM4G'^s_t>Wr\kSpi^7:PK$HV0h䞟mc~ޘ6 Fm %s?]%c Qj[#>0=&&8?+_fQ!aGb<$كC/Fʖ]#wDK1K{)]Txd;Vׄahj!]D ;^W{~DOdžĕԆ`, vO5Nˆ/}$omB ϛ їw`DoU" Lx96[?n@w#gdE*gXu:h]@z}Is4 @QD2&|gP){Lc Y|;6ph0).=>>"#bQIk2  U7 {rUo|S|eDxƒ@p!4@\<z̀R9ZXnۭC}ѤqfE〛Sj ] O}8l΢tB|@v?}GQNM|$ZOЗg p MMӍ7 s01Tܧh._Ddx&@x4ﰣ8>:id.-4sUQ66^} 6^$tϫjj{Stq 1Tp! u> v"i ڂ~`v|7~O7 _X3(J30˜V%s<34r?_R$.٬?0jyD2(?>tnk < *44)&C}^^/~`="IKft] 9yFZYEZJfiW> YB]q3IW򵿒B9"*?V0c$@2nSK|-2X4yb Ƞ\4:T):4|E(e.DŢ~/6& ]-aMSҡVJ)ѭB!рQ ~H-'=_laU1WUsf V5B1$2^M2>?h|^ܦ+DJc }: j2EA$ U"0?  <w!郯&87V3#uwA+vnNd^ '9(|  ׂeeٽ{F c3.T*՚F˗/wwgpOF9F[(31H={q?GC%T@JIHP>.د93>M\\~b#3tWޮhzy׿0#P*5^JiǠ7_!i!$ %]z[)$bwuS-WAe '_$#G~+AC܀ 2*`rt1A?riSc95ahFbŊij:sJJ+** 3I)),Yd$IG6WR+{ǷIOG>L\<%ʥbf3GgSom\FNB" !-h284P0xyAWyJ0h  EZI墢5t(ɴ@cC Z[{1|6ɤ_Aw.[:5Ȟ_co M *K6'yMF0}l0.g_=?n]}G㮉 %e9̭ؠ`ܹY$1 [/ @II7N,<3))HTVX(x]w3RM MUVp!Ƅ)X)/xеy~* @ | 07榏  게aAOЪ% rhm ud늼%IJ7o%%%nhh$'2l6*776I]uU_S2m4Νˑ#G6'kmnVFw^z9Vjtf7f빡PSSNcY̝U%kP02Mѵ|OLU e p[=OCt*2SHXQ bs%;nNSof̢Ncrn~%&fM:hkcě~?16 HIDAT/| TVVr233GcaIEx}݇dB$J%ӧOG$n6z)-2JыlX DL]ǁǮ3\ ww6ELҍ/㍭ԜhJ?mb"aC0@b !bj"s9Љ=+[(s%hE*[4"˸J >˷/+B0m|n׬( { K0tÁA,|;ئ+gǎ '@C\H Z166$MڿhEUQmR564mG%t@~ABNqlsy8{s9sXIQQ[˩S Nmm[V$Iz4|BE$Iѣ$IR A9r`0hûn߾}_ } oo7xΏN;0݁a>ZuXU*VU.*~(52~T\4fl-h3"`%u%BJd|_-v T {5i[ #vzguA혓oH?§ BMZ)h"A&f'mmEsNΜ9$IXVDQ@ HYvӦM=&ɹw^o>ƫĉ˗/7;q/ErFEYɾ6-=Ă7zI $vq}Ii{yv4kbhRjX0ä T|C%' V- 4mnQoHAvzP7!.]Jl}-uuL1DQdzu尬 AX,\~ .\CuF A fKejȲL**^{F___jۺԳge9Klx!t(0&2Hgc|JOR*3~a~՚ϔNጅp)/OubfM.0{rygTY'+4WqE6*1/YBP(6穮Fw)B6Ν; F  cccjbxp:(2{h4044tNy}5p274ɭ]!8KYpn8R1T4|?: t.gO b7_a9Q`}jlVK aZvM v SS B@UUnjkkf 088wJ5FFF>wM&ӷfrlL&C:fzzt:M pv_(`U-g#tK9K4 D_*GٓUBU2e3q5KLp\" Y`*}oPjFi -c4awT) eSSX.]z,7Cb!W__֭[`||˅ D" l62<<>W(z[ ) p8p8.ʲP($|>?)ǯ2@4K @)stlO$cLFdiȽ8 eVZfRXŸuiE&r$'+Lߏ;^%q#9MPwM?]]]8N"XL&3z?4TaVG~f[(( 2]]]9sR{}\MZ/25n6/Խ#n7eea?}eo06Fv6Ç_~K[wezXn ǯd [$bPU |>͛7Y4j']"I: krT+]akЬz{^zÌ::Ĉ$?y]ذa^"Q|L] `XWW+l^x.3z8xt37oޜYimmڵkȫVO$dIс5+Vt:w@Q۔y=LmA5R694^{S_U477ˑ\*|>/bOz{{?H& ZpN92]aag0EIeL>SAQ1TFm/z{{L&Cz RRW֘c鴐E(D._/(HӣX&.P< (O$SJdF5]3ڽ 23c!zsBȸIIIENDB`hgview-1.9.0/hgviewlib/qt4/icons/openexternal.png0000644000015700001640000000126412607505500022620 0ustar narvalnarval00000000000000PNG  IHDR szz{IDATx^WnP{}]Ue(*5lEW%~%E#6lohU)"Mbw2ƎvJe s)!I-`/@mۖt^?LR"~V5r,>ExW*xH.,@g~; 2"EpTÆ Jg F䦂^)++RdbRAd7|Tѯ_>'PJ% 0 EuYd2f e% 2uRHnn'j5Wz=H]&?;>DM?XDכ7 i@Z- 0M*1וP f;Ja˅*g4LJ>@Fb&$U%ʔF"Y1 "6!-KPZ4odx;>9ae)a K@6$ 8&`P(@ۅ\.dr6WwXd  s5 *WRcjR 1DL%tFr.Hi1a۶5>(1 O Gg- aFxNl1ln2D,9f\XPQIENDB`hgview-1.9.0/hgviewlib/qt4/icons/close.png0000644000015700001640000000200712607505500021215 0ustar narvalnarval00000000000000PNG  IHDRĴl;sRGBbKGD pHYs  tIME PIDAT8˝oU?gfޯhВH !jn Qv*K+\101jƤ,Rk"x?73o?^I̝ܹ{~gF."::ɦrⰧP&I'i4z|1@@mjj4n"+ @~hl%7 n q\T@B+D 4OL& *H͡2g,P\؀k&e3 8D␬ x4 hx}!eYzBM ^_ug8Jm1 J@2zL} $ py_' Ĩgq{p2g!Hb{ME`胻vzxTO"] $ ^h q}rEXL~N> l8DŅ^ yث?s-V!7/?CW5GUh_8"ǡgpѥi̥kl`V@ԀjGAݸ%^{ ,4ENGG}T C*o 7< r5(4'S3tw;9UПm8o~lY[.Iz(׉3INlޚ@o^d$r zZ١bC2y4[ ,׾DۅK"#*Kh$m`Γ,&61yf z @-pIJ|HVK& U).6'ƺ:Pm˿-6'窔@vp$0aDy~X٨- t=0@T*7QBے'2IENDB`hgview-1.9.0/hgviewlib/qt4/icons/left.svg0000644000015700001640000000633112607505500021061 0ustar narvalnarval00000000000000 image/svg+xml hgview-1.9.0/hgviewlib/qt4/icons/mqdiff_x.svg0000644000015700001640000002024512607505500021724 0ustar narvalnarval00000000000000 image/svg+xml ! hgview-1.9.0/hgviewlib/qt4/icons/valid.png0000644000015700001640000000103112607505500021203 0ustar narvalnarval00000000000000PNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<IDAT8˽.Ca{8bnSBT')E)VCJ ǥj ZՆg/h…ݿk n^k[ꝿ2P6c=XH*G`?xԅ{77VԨپ%VHyqNtn[J2^53X,S-OƜoDXx2Oܵ r]L`}Z࿳TU(SiP/a:6͖,A` %S=[ b[a='LaW{xD[ u9J—BGqzfGN0os6"ffhZR".2H-[{(7h @`%E[IWu3e+ lGQ&' k|J~9F1̷cn#ôᵵc#c5,7/bj++>3 cW7]c]C`j}{i4 Ffs~3i>M@5A~fKſi<ο-z2?~~":X]/#|m\ڂ\6htbErj; Q >AM*pG9 ̅rE:=;EMDH=]X}(%fN%AЅco/+3+SD|Ɖ~oO 9hqQt9)Ns!Ç槧bu ]PI 8}"h&MBQq;uT,CiU]vSSm!s8* !Bt(t!3Lwa tW Ft j@L!1ߜNfL4ޯ*J.gB%ßy?޿$&1^R<1Z@.,ĩۜkOi6L #߳VdTtgɷۛ-h5# a!wT{HCdeا"x{M\'۝N Fz>,Ɲo`/Ij[ZZf),r=XCAmoGw6]`o!> $']tvt],;;g8߀ н V=z؆Z0Ɔh R{7Dbxc b,x1>jƅWx( 6'PIENDB`hgview-1.9.0/hgviewlib/qt4/icons/README0000644000015700001640000000167112607505500020270 0ustar narvalnarval00000000000000Most of the icons used here are from the Tango Icon Theme. Some of them have been modified. showhide.png: Gnome Project (GPL) [http://art.gnome.org/themes/icon] content.png: by PC (Creative Common v3) [http://www.iconfinder.com/icondetails/59552/32/cv_icon] diffmode.png: by FatCow (Creative Common v3) [http://www.fatcow.com/free-icons] unfilter.png: Everaldo Coelho (GPL) [http://www.veryicon.com/icons/system/crystal-clear-actions/] heavy.png: Everaldo Coelho (GPL) [http://www.iconfinder.com/icondetails/15543/128/animation_fat_run_icon] valid.png: Mark James (CC-by) http://www.iconfinder.com/icondetails/5880/16/active_check_green_ok_right_tick_icon loading.png: by PC (CC-by) [http://www.iconfinder.com/icondetails/59192/32/loader_loading_process_spinner_icon] openexternal: by FatCow (CC-by) http://www.iconfinder.com/icondetails/63957/32/card_export_icon hgview-1.9.0/hgviewlib/qt4/icons/heavy_small.png0000644000015700001640000000205412607505500022416 0ustar narvalnarval00000000000000PNG  IHDRa pHYs  gAMA|Q cHRMz%u0`:o_FIDATxb`ؤD\,420C) fle6}YkeΕlY]8Ga#ZBAbfUm&j~nopKL [Ҙx `wYCV]3xql{;x10ZiPybbM1D?__G=y-=ɿ?b]UF30间/lŕ @,l O", u q# gxt* \\v n^g;jAOƙ  0 f姼g_fHiXP!K 4m 5u>(p0f#|gi X32~dE7>` YIa{c?$1p3Vp ;Ŧ "Zgm, . Ŋ @$cR  S L29`$30 ?14gZ3y ,aQ@,Ƭ 2A ;+ /Nws? 㛺0A*H<Ư\-'.= 0AOGG|\ AĸW3zB՛Ϥ˰Ø3WjAsqiy+kd13a5ah򵠮6fdHotq'-  vb~‚jY"  h;w$QIENDB`hgview-1.9.0/hgviewlib/qt4/icons/content.png0000755000015700001640000000240112607505500021563 0ustar narvalnarval00000000000000PNG  IHDR szztEXtSoftwareAdobe ImageReadyqe<IDATxڼWOTG?FȲ IEETDzJŏH7?@_iMՈ ~$K/M(l)=ٹc93sΙوmۄs&wȧݚؗG t,KPNQ{\^u_V\)˴KQi@BN:n3UϠQD@$XXUý{Xog޴V\4 x<>3=yr\,Ut|x}F7IY=,@bFp.ǂKe2fiz{"3 ,PAJ/-/ӯ/^uSU~L崯RT[]MMM4A5r_ _Xk7oR V[  QΝcpO50,ok/>HhT߯^Qzz]6ǀWe,rq p#/ .t6Tn;@8'Ek,5hrc _DB9Lz"u5dbj5S/y3vePDYo4>.terJ8Nj8>q]H.ВQE<Nz rhF3$o:4#褞afƤpaRr hWxa֘d/-. u >ٲ80=xr}{cj6}V_J@/8z\JhTX%t }uv+X$m}9pm ? 0+{ƛ"IENDB`hgview-1.9.0/hgviewlib/qt4/icons/up.svg0000644000015700001640000001777312607505500020567 0ustar narvalnarval00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Up go higher up arrow pointer > Andreas Nilsson hgview-1.9.0/hgviewlib/qt4/icons/back.svg0000644000015700001640000001751312607505500021033 0ustar narvalnarval00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Previous go previous left arrow pointer < hgview-1.9.0/hgviewlib/qt4/icons/mqpatch.svg0000644000015700001640000001777312607505500021600 0ustar narvalnarval00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz Go Up go higher up arrow pointer > Andreas Nilsson hgview-1.9.0/hgviewlib/qt4/icons/reload.svg0000644000015700001640000001560412607505500021400 0ustar narvalnarval00000000000000 ]> hgview-1.9.0/hgviewlib/qt4/icons/modified.svg0000644000015700001640000007655212607505500021723 0ustar narvalnarval00000000000000 image/svg+xml ! hgview-1.9.0/hgviewlib/qt4/icons/quit.svg0000644000015700001640000002343612607505500021116 0ustar narvalnarval00000000000000 ]> hgview-1.9.0/hgviewlib/qt4/icons/help.svg0000644000015700001640000000612312607505500021056 0ustar narvalnarval00000000000000 ]> hgview-1.9.0/hgviewlib/qt4/icons/right.svg0000644000015700001640000000634212607505500021246 0ustar narvalnarval00000000000000 image/svg+xml hgview-1.9.0/hgviewlib/qt4/icons/mqpatch_x.svg0000644000015700001640000001054312607505500022113 0ustar narvalnarval00000000000000 image/svg+xml hgview-1.9.0/hgviewlib/qt4/icons/goto.svg0000644000015700001640000005352112607505500021102 0ustar narvalnarval00000000000000 image/svg+xml Save Jakub Steiner hdd hard drive save io store http://jimmac.musichall.cz hgview-1.9.0/hgviewlib/qt4/hgqv.qrc0000644000015700001640000000204112607505500017741 0ustar narvalnarval00000000000000 icons/quit.svg icons/reload.svg icons/back.svg icons/forward.svg icons/left.svg icons/right.svg icons/up.svg icons/down.svg icons/leftright.svg icons/close.png icons/help.svg icons/find.svg icons/goto.svg icons/modified.svg icons/clean.svg icons/mqdiff.svg icons/mqdiff_x.svg icons/mqpatch.svg icons/mqpatch_x.svg icons/showhide.png icons/content.png icons/unfilter.png icons/diffmode.png icons/heavy.png icons/heavy_small.png icons/valid.png icons/loading.png resources/description.css icons/openexternal.png hgview-1.9.0/hgviewlib/qt4/hgqv.ui0000644000015700001640000001716312607505500017604 0ustar narvalnarval00000000000000 MainWindow 0 0 730 646 hgview 0 0 Qt::Vertical QFrame::StyledPanel 0 0 QFrame::NoFrame QFrame::Plain 0 Qt::Horizontal Qt::Vertical 0 1 0 2 0 0 730 26 &File &Help &Edit Window toolbar TopToolBarArea false true QuickOpen toolbar TopToolBarArea false true Navigation toolbar TopToolBarArea false true Filter toolbar TopToolBarArea false Diff toolbar TopToolBarArea false Revision toolbar TopToolBarArea false Help toolbar TopToolBarArea false &Open repository Ctrl+O &Refresh Ctrl+R &Quit Quit Ctrl+Q About displayAllBranches Help RevisionsTableView QTableView
revisions_table.h
RevisionDescriptionView QTextBrowser
revision_description.h
HgFileListView QTableView
hgfileview.h
HgFileView QWidget
hgfileview.h
1
hgview-1.9.0/hgviewlib/qt4/mixins.py0000644000015700001640000002132612607505500020155 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . # # make sure the Qt rc files are converted into python modules, then load them # this must be done BEFORE other hgview qt4 modules are loaded. import os import os.path as osp import sys from PyQt4 import uic from PyQt4.QtCore import QTimer, Qt from PyQt4.QtGui import QAction, QActionGroup, QMenu, QShortcut from hgviewlib.config import HgConfig from hgviewlib.qt4 import should_rebuild from hgviewlib.qt4 import icon as geticon from hgviewlib.qt4.config import get_font class ActionsMixin(object): """ A Mixin for Hgview widgets containing a simple way to create and register Qt actions. """ def __init__(self, *args, **kwargs): super(ActionsMixin, self).__init__(*args, **kwargs) self._actions = {} self._action_groups = {} self._action_groups_order = [] def _set_action(self, name, **parameters): """Set the registred At action named ``name``. ``parameters`` are optional arguments given to ``add_action``. """ action = self._actions[name] if parameters.get('icon'): action.setIcon(geticon(parameters['icon'])) if parameters.get('tip'): action.setStatusTip(parameters['tip']) if parameters.get('keys'): action.setShortcuts(parameters['keys']) if parameters.get('checked') is not None: action.setCheckable(True) action.setChecked(parameters['checked']) action.setEnabled(parameters.get('enabled', True)) if parameters.get('callback'): if parameters.get('checked') is not None: signal = action.toggled[bool] else: signal = action.triggered signal.connect(parameters['callback']) menu = parameters.get('menu', None) if menu is not None: if menu not in self._action_groups: group = QActionGroup(self) self._action_groups[unicode(menu)] = group self._action_groups_order.append(menu) group.addAction(menu).setSeparator(True) else: group = self._action_groups[unicode(menu)] group.setExclusive(False) group.addAction(action) def set_action(self, name, **parameters): """Set the registred At action named ``name``. ``parameters`` are optional arguments given to ``add_action``. """ # delayed to not append after add_action QTimer.singleShot(0, lambda: self._set_action(name, **parameters)) def set_actions(self, *names, **parameters): """Same as set_actions but accept multiple registred actions names as positioned arguments. """ for name in names: self.set_action(name, **parameters) def add_action(self, name, action, **options): """Add and attach an action to the widget. The action setup is finalized in a dedicated thread in order to speed up the hgview start up. .. note:: the action is added to the context menu of the widget if the ``menu`` option is specified. Arguments --------- :name: action name :action: a QAction object or a short desciption string. Optional arguments ------------------ :menu: a string containing the group name of the action in the context menu. The action is not added to context menu if set to None (default). :checked: A boolean to set the action as checkable or None. The boolean value is used for the action checked status. (default: None == non checkable). :enabled: A boolean value used for the enabled status (default True) :callback: a slot called on the ``triggered()`` signal (or ``toggled[bool]`` if ``checkable is True) :icon: registred icon name as expected by ``hgviewlib.qt4.icon(icon)`` :keys: a single keybinding or a list of keybindings (see Qt bindings definition) :tip: extended desciption string """ if not isinstance(action, QAction): action = QAction(action, self) self._actions[name] = action self.addAction(action) QTimer.singleShot(0, lambda: self._set_action(name, **options)) return action def get_action(self, name): """Return the registred qt action object named ``name``.""" return self._actions[name] def get_actions(self, *names): """Return a tuple of registred qt actions named ``names``.""" return tuple(self.get_action(name) for name in names) def contextMenuEvent(self, event): """Overwritte context menu event to add registred actions.""" try: menu = self.createStandardContextMenu() except AttributeError: menu = QMenu(self) for name in self._action_groups_order: group = self._action_groups[name] menu.addActions(group.actions()) menu.exec_(event.globalPos()) def ui2cls(ui_name): """Compile the .ui file named ``ui_name`` into a python class en return it. Also tyr to comvert the .ui file into a .py file in order to generate the python code only once. This allow to speed up the hgview start up as the python module can be imported directly on next start up. """ _path = osp.dirname(__file__) uifile = osp.join(_path, ui_name) pyfile = uifile.replace(".ui", "_ui.py") if should_rebuild(uifile, pyfile): os.system('pyuic4 %s -o %s' % (uifile, pyfile)) try: modname = osp.splitext(osp.basename(uifile))[0] + "_ui" modname = "hgviewlib.qt4.%s" % modname mod = __import__(modname, fromlist=['*']) classnames = [x for x in dir(mod) if x.startswith('Ui_')] if len(classnames) == 1: ui_class = getattr(mod, classnames[0]) elif 'Ui_MainWindow' in classnames: ui_class = getattr(mod, 'Ui_MainWindow') else: raise ValueError("Can't determine which main class to use in %s" % modname) except ImportError: ui_class, base_class = uic.loadUiType(uifile) return ui_class class HgDialogMixin(object): """ Mixin for QDialogs defined from a .ui file, which automates the setup of the UI from the ui file, and the loading of user preferences. The main class must define a '_uifile' class attribute. """ def __init__(self, *args, **kwargs): self._cfg = None self._font = None super(HgDialogMixin, self).__init__(*args, **kwargs) def load_ui(self): self.setupUi(self) # we explicitly create a QShortcut so we can disable it # when a "helper context toolbar" is activated (which can be # closed by hitting the Esc shortcut) self.esc_shortcut = QShortcut(self) self.esc_shortcut.setKey(Qt.Key_Escape) self.esc_shortcut.activated.connect(self.maybeClose) self._quickbars = [] self.disab_shortcuts = [] def attachQuickBar(self, qbar): qbar.setParent(self) self._quickbars.append(qbar) qbar.esc_shortcut_disabled[bool].connect(self.setShortcutsEnabled) self.addToolBar(Qt.BottomToolBarArea, qbar) qbar.unhidden.connect(self.ensureOneQuickBar) def setShortcutsEnabled(self, enabled=True): for sh in self.disab_shortcuts: sh.setEnabled(enabled) def ensureOneQuickBar(self): tb = self.sender() for w in self._quickbars: if w is not tb: w.hide() def maybeClose(self): for w in self._quickbars: if w.isVisible(): w.cancel() break else: self.close() def load_config(self, repo): self.cfg = HgConfig(repo.ui) self._font = get_font(self.cfg) self.rowheight = self.cfg.getRowHeight() self.users, self.aliases = self.cfg.getUsers() def accept(self): self.close() def reject(self): self.close() hgview-1.9.0/hgviewlib/qt4/hgrepoviewer.py0000644000015700001640000006762012607505500021363 0ustar narvalnarval00000000000000# -*- coding: iso-8859-1 -*- # main.py - qt4-based hg rev log browser # # 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 hgview """ import sys, os import os.path as osp import re import errno from functools import partial from operator import methodcaller from PyQt4 import QtCore, QtGui, Qsci from mercurial import ui, hg from mercurial import util from mercurial.error import RepoError from hgviewlib.application import HgRepoViewer as _HgRepoViewer from hgviewlib.util import (tounicode, find_repository, rootpath, build_repo, upward_path, read_nested_repo_paths, allbranches) from hgviewlib.hggraph import diff as revdiff from hgviewlib.decorators import timeit from hgviewlib.config import HgConfig from hgviewlib.util import compose from hgviewlib.qt4.hgrepomodel import HgRepoListModel, HgFileListModel from hgviewlib.qt4.hgfiledialog import FileViewer, FileDiffViewer from hgviewlib.qt4.hgmanifestdialog import ManifestViewer from hgviewlib.qt4.mixins import HgDialogMixin, ActionsMixin, ui2cls from hgviewlib.qt4.quickbar import FindInGraphlogQuickBar from hgviewlib.qt4.helpviewer import HgviewHelpViewer from hgviewlib.hgpatches import hiddenrevs from mercurial.error import RepoError Qt = QtCore.Qt bold = QtGui.QFont.Bold NESTED_PREFIX = u'\N{RIGHTWARDS ARROW} ' class HgRepoViewer(ActionsMixin, HgDialogMixin, ui2cls('hgqv.ui'), QtGui.QMainWindow, _HgRepoViewer): """hg repository viewer/browser application""" def __init__(self, repo, fromhead=None): self.repo = repo # these are used to know where to go after a reload self._reload_rev = None self._reload_file = None QtGui.QApplication.setStyle(QtGui.QStyleFactory.create('Cleanlooks')) super(HgRepoViewer, self).__init__() self.load_ui() if not self.repo.root: repopath = self.ask_repository() self.repo = build_repo(ui.ui(), repopath) self.load_config(self.repo) self.setWindowTitle('hgview: %s' % os.path.abspath(str(self.repo.root))) self.menubar.hide() self.splitter_2.setStretchFactor(0, 2) self.splitter_2.setStretchFactor(1, 1) # hide bottom at startup self.frame_maincontent.setVisible(self.cfg.getContentAtStartUp()) self.createActions() self.createToolbars() self.textview_status.setFont(self._font) self.textview_status.message_logged.connect( self.statusBar().showMessage) self.tableView_revisions.message_logged.connect( self.statusBar().showMessage) # setup tables and views if self.repo.root is not None: self.setupHeaderTextview() self.setupBranchCombo() self.setupModels(fromhead) self._setupQuickOpen() to_utf8 = methodcaller('encode', 'utf-8') if self.cfg.getFileDescriptionView() == 'asfile': fileselcallback = compose(self.displaySelectedFile, to_utf8) else: fileselcallback = compose(self.textview_status.displayFile, to_utf8) self.tableView_filelist.file_selected[str].connect(fileselcallback) self.tableView_filelist.file_selected[str, int].connect(fileselcallback) self.textview_status.rev_for_diff_changed.connect( self.textview_header.setDiffRevision) if fromhead: self.startrev_entry.setText(str(fromhead)) self.setupRevisionTable() if self.repo.root is not None: self._repodate = self._getrepomtime() self._watchrepotimer = self.startTimer(500) def timerEvent(self, event): if event.timerId() == self._watchrepotimer: mtime = self._getrepomtime() if mtime > self._repodate: self.statusBar().showMessage("Repository has been modified, " "reloading...", 2000) self.reload() def contextMenuEvent(self, event): # The contextMenuEvent in this qt widgets automatically add actions. # If we rewrite it (as done in ActionsMixin), we cannot conserve # the original menu :/ QtGui.QMainWindow.contextMenuEvent(self, event) def _setupQuickOpen(self): """Initiliaze the quick open menu This call utils function to search for possible nested repo setup""" # quick open repository self.quickOpen_comboBox.clear() repopath = osp.abspath(self.repo.root) # search backward until the root folder for a master # repository that contains confman or subrepo data for master_path in upward_path(repopath): if not osp.exists(osp.join(master_path, '.hg')): continue # Not a repo! subrepos = read_nested_repo_paths(master_path) if not subrepos: continue # Nothing nested! # We found a nested repo setup! # # But is it related to the viewed repo? involved_paths = [master_path] + [pth for _, pth in subrepos] if repopath in involved_paths: master_name = tounicode(osp.basename(master_path)) repos = [(master_name, master_path)] for name, path in subrepos: repos.append((NESTED_PREFIX + name, path)) repos.sort() break # I've found related! I'll survive the glaciation! else: # They migrated without me. They do this every year. # # Nothing to quick-open hide the toolbar and interrupt self.toolBar_quickopen.setVisible(False) return self.toolBar_quickopen.setVisible(True) # TODO: add a treeview with a complete graph. curidx = 0 for idx, (text, data) in enumerate(repos): if data == self.repo.root: curidx = idx self.quickOpen_comboBox.addItem(text, data) self.quickOpen_comboBox.setCurrentIndex(curidx) def setupBranchComboAndReload(self, *args): self.setupBranchCombo() self.reload() def setupBranchCombo(self, *args): branches = allbranches(self.repo, self.branch_checkBox_action.isChecked()) if len(branches) == 1: self.branch_label_action.setEnabled(False) self.branch_comboBox_action.setEnabled(False) else: self.branchesmodel = QtGui.QStringListModel([''] + branches) self.branch_comboBox.setModel(self.branchesmodel) self.branch_label_action.setEnabled(True) self.branch_comboBox_action.setEnabled(True) def createToolbars(self): # find quickbar self.find_toolbar = FindInGraphlogQuickBar(self) self.find_toolbar.attachFileView(self.textview_status) self.find_toolbar.attachHeaderView(self.textview_header) self.find_toolbar.revision_selected.connect( self.tableView_revisions.goto) self.find_toolbar.revision_selected[int].connect( self.tableView_revisions.goto) self.find_toolbar.file_selected.connect( self.tableView_filelist.selectFile) self.find_toolbar.message_logged.connect( self.statusBar().showMessage, Qt.QueuedConnection) self.attachQuickBar(self.find_toolbar) # navigation toolbar self.toolBar_edit.addAction(self.get_action('content')) self.toolBar_edit.addActions( self.tableView_revisions.get_actions('back', 'forward')) findaction = self.add_action( 'find', self.find_toolbar.toggleViewAction(), menu=self.tr("edit"), icon='find', keys=['/', 'Ctrl+f'], tip=self.tr("Search text in all revisions metadata"), ) self.toolBar_edit.addAction(findaction) cb = self.quickOpen_comboBox = QtGui.QComboBox() cb.setStatusTip("Quick open other repositories") self.quickOpen_action = self.toolBar_quickopen.addWidget(cb) _callback = lambda: self.open_repository(cb.itemData(cb.currentIndex())) self.quickOpen_comboBox.activated.connect(_callback) # tree filters toolbar self.toolBar_treefilters.addAction(self.get_action('showhide')) self.toolBar_treefilters.addAction(self.tableView_revisions.get_action('unfilter')) self.toolBar_treefilters.addAction(self.get_action('showhide')) self.branch_label = QtGui.QToolButton() self.branch_label.setText("Branch") self.branch_label.setStatusTip("Display graph the named branch only") self.branch_label.setPopupMode(QtGui.QToolButton.InstantPopup) self.branch_menu = QtGui.QMenu() cbranch_action = self.branch_menu.addAction("Display closed branches") cbranch_action.setCheckable(True) self.branch_checkBox_action = cbranch_action self.branch_label.setMenu(self.branch_menu) self.branch_comboBox = QtGui.QComboBox() self.branch_comboBox.activated[str].connect(self.refreshRevisionTable) cbranch_action.toggled[bool].connect(self.setupBranchComboAndReload) self.toolBar_treefilters.layout().setSpacing(3) self.branch_label_action = self.toolBar_treefilters.addWidget(self.branch_label) self.branch_comboBox_action = self.toolBar_treefilters.addWidget(self.branch_comboBox) # separator self.toolBar_treefilters.addSeparator() self.startrev_label = QtGui.QToolButton() self.startrev_label.setText("Start rev.") self.startrev_label.setStatusTip("Display graph from this revision") self.startrev_label.setPopupMode(QtGui.QToolButton.InstantPopup) self.startrev_entry = QtGui.QLineEdit() self.startrev_entry.setStatusTip("Display graph from this revision") self.startrev_menu = QtGui.QMenu() follow_action = self.startrev_menu.addAction("Follow mode") follow_action.setCheckable(True) follow_action.setStatusTip("Follow changeset history from start revision") self.startrev_follow_action = follow_action self.startrev_label.setMenu(self.startrev_menu) callback = lambda *a: self.tableView_revisions.start_from_rev[str, bool].emit( str(self.startrev_entry.text()), self.startrev_follow_action.isChecked()) self.startrev_entry.editingFinished.connect(callback) self.startrev_follow_action.toggled[bool].connect(callback) self.revscompl_model = QtGui.QStringListModel(['tip']) self.revcompleter = QtGui.QCompleter(self.revscompl_model, self) self.startrev_entry.setCompleter(self.revcompleter) self.startrev_label_action = self.toolBar_treefilters.addWidget(self.startrev_label) self.startrev_entry_action = self.toolBar_treefilters.addWidget(self.startrev_entry) # diff mode toolbar actions = self.textview_status.sci.get_actions( 'diffmode', 'prev', 'next', 'show-big-file') self.toolBar_diff.addActions(actions) # rev mod toolbar self.toolBar_rev.addAction(self.textview_header.get_action('rst')) self._handle_toolbar_visibility() self.addAction(self.actionQuit) def _handle_toolbar_visibility(self): """Initial value and event hooking This function read toolbar related persistent settings from QT API. It also setup hooks on visibility changes so to make the setting persistent. """ toolbars = (self.toolBar_file, self.toolBar_quickopen, self.toolBar_edit, self.toolBar_treefilters, self.toolBar_diff, self.toolBar_rev, self.toolBar_help) settings = QtCore.QSettings() for toolbar in toolbars: entryname = '%s/%s/visible' % (self.objectName(), toolbar.objectName()) # bring back persistent status status = settings.value(entryname) if status is None: status = 1 else: try: status = int(status) except ValueError: # for backward compatibility status = {'false': False , 'true': True}.get(status.lower(), status) status = int(status) toolbar.setVisible(status) # update settings on visibility toggleling # We use integers because PyQt4 setting stores boolean as strings # with QVariant api version 2 toolbar.toggleViewAction().toggled[bool].connect( compose(partial(settings.setValue, entryname), int) ) def createActions(self): # main window actions (from .ui file) self.add_action( 'open', self.actionOpen_repository, tip=self.tr("Change watched repository"), callback=self.open_repository, ) self.add_action( 'refresh', self.actionRefresh, icon='reload', tip=self.tr("Refresh watched repository metadata"), callback=self.reload, ) self.add_action( 'quit', self.actionQuit, icon='quit', tip=self.tr("Quit Hgview"), callback=self.close, ) self.add_action( 'help', self.actionHelp, icon='help', tip=self.tr("Display Hgview help"), callback=self.on_help, ) act = self.add_action( 'showhide', self.tr("self.tr('Show/Hide Hidden')"), icon='showhide', tip=self.tr('Show/Hide hidden changeset'), callback=self.refreshRevisionTable, checked=bool(self.cfg.getShowHidden()), ) act = self.add_action( 'content', self.tr("Content"), icon='content', tip=self.tr('Show/Hide changeset content'), keys=[Qt.Key_Space], callback=self.toggleMainContent, checked=bool(self.cfg.getContentAtStartUp()), ) # Next/Prev file act = self.add_action( 'nextfile', self.tr("Next file"), icon='back', tip=self.tr("Select the next file"), keys=['Right'], callback=self.tableView_filelist.nextFile ) self.disab_shortcuts.append(act) act = self.add_action( 'prevfile', self.tr("Previous file"), icon='forward', tip=self.tr("Select the previous file"), keys=['Left'], callback=self.tableView_filelist.prevFile ) self.disab_shortcuts.append(act) # Next/Prev rev act = self.add_action( 'nextrev', self.tr("Next revision"), icon='down', tip=self.tr("Select the next revision in the graph"), keys=['Down'], callback=self.tableView_revisions.nextRev ) self.disab_shortcuts.append(act) act = self.add_action( 'prevrev', self.tr("Previous revision"), icon='up', tip=self.tr("Select the previous revision in the graph"), keys=['Up'], callback=self.tableView_revisions.prevRev ) self.disab_shortcuts.append(act) # navigate in file viewer self.add_action( 'next-line', self.tr('Next line'), tip=self.tr('Select the next line'), keys=[Qt.SHIFT + Qt.Key_Down], callback=self.textview_status.nextLine, ) self.add_action( 'prev-line', self.tr('Previous line'), tip=self.tr('Select the previous line'), keys=[Qt.SHIFT + Qt.Key_Up], callback=self.textview_status.prevLine, ) self.add_action( 'next-column', self.tr('Next column'), tip=self.tr('Select the next column'), keys=[Qt.SHIFT + Qt.Key_Right], callback=self.textview_status.nextCol, ) self.add_action( 'prev-column', self.tr('Previous column'), tip=self.tr('Select the previous column'), keys=[Qt.SHIFT + Qt.Key_Left], callback=self.textview_status.prevCol, ) # Activate file (file diff navigator) def enterkeypressed(): w = QtGui.QApplication.focusWidget() if not isinstance(w, QtGui.QLineEdit): self.tableView_filelist.fileActivated(self.tableView_filelist.currentIndex(),) else: w.editingFinished.emit() self.add_action( 'activate-file', self.tr('Activate file'), tip=self.tr('Activate the file'), keys=[Qt.Key_Return, Qt.Key_Enter], callback=enterkeypressed, ) def altenterkeypressed(): self.tableView_filelist.fileActivated(self.tableView_filelist.currentIndex(), alternate=True) self.add_action( 'activate-alt-file', self.tr('Activate alt. file'), tip=self.tr('Activate alternative file'), keys=[Qt.ALT+Qt.Key_Return, Qt.ALT+Qt.Key_Enter], callback=altenterkeypressed, ) def toggleMainContent(self, visible=None): if visible is None: visible = self.get_action('content').isChecked() visible = bool(visible) if visible == self.frame_maincontent.isVisible(): return self.set_action('content', checked=visible) self.frame_maincontent.setVisible(visible) if visible: self.revision_selected(-1) def setMode(self, mode): self.textview_status.setMode(mode) def load_config(self, repo): super(HgRepoViewer, self).load_config(repo) self.hidefinddelay = self.cfg.getHideFindDelay() def create_models(self, fromhead=None): self.repomodel = HgRepoListModel(self.repo, fromhead=fromhead) self.repomodel.filled.connect(self.on_filled) self.repomodel.message_logged.connect( self.statusBar().showMessage, Qt.QueuedConnection) self.filelistmodel = HgFileListModel(self.repo, parent=self) def setupModels(self, fromhead=None): self.create_models(fromhead) self.tableView_revisions.setModel(self.repomodel) self.tableView_filelist.setModel(self.filelistmodel) self.textview_status.setModel(self.repomodel) self.find_toolbar.setModel(self.repomodel) def displaySelectedFile(self, filename=None, rev=None): if filename == '': self.textview_status.hide() self.textview_header.show() else: self.textview_header.hide() self.textview_status.show() self.textview_status.displayFile(filename, rev) def setupRevisionTable(self): view = self.tableView_revisions view.installEventFilter(self) view.clicked.connect(self.toggleMainContent) view.revision_selected.connect(self.revision_selected) view.revision_selected[int].connect(self.revision_selected) view.revision_activated.connect(self.revision_activated) view.revision_activated[int].connect(self.revision_activated) view.start_from_rev.connect(self.start_from_rev) view.start_from_rev[int, bool].connect(self.start_from_rev) view.start_from_rev[str, bool].connect(self.start_from_rev) self.textview_header.revision_selected.connect(view.goto) self.textview_header.revision_selected[int].connect(view.goto) self.textview_header.parent_revision_selected.connect( self.textview_status.displayDiff) self.textview_header.parent_revision_selected[int].connect( self.textview_status.displayDiff) self.attachQuickBar(view.goto_toolbar) gotoaction = self.add_action( 'goto', view.goto_toolbar.toggleViewAction(), icon='goto', keys=['Ctrl+g'], tip=self.tr("Search for revision in the graph"), ) self.toolBar_edit.addAction(gotoaction) def start_from_rev(self, rev=None, follow=False): rev = rev or None # '' => None self.startrev_entry.setText(str(rev or '')) self.startrev_follow_action.setChecked(follow) self.refreshRevisionTable(rev=rev, follow=follow) def _setup_table(self, table): table.setTabKeyNavigation(False) table.verticalHeader().setDefaultSectionSize(self.rowheight) table.setShowGrid(False) table.verticalHeader().hide() table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) table.setAlternatingRowColors(True) def setupHeaderTextview(self): self.header_diff_format = QtGui.QTextCharFormat() self.header_diff_format.setFont(self._font) self.header_diff_format.setFontWeight(bold) self.header_diff_format.setForeground(Qt.black) self.header_diff_format.setBackground(Qt.gray) def on_filled(self): # called the first time the model is filled, so we select # the first available revision tv = self.tableView_revisions if self._reload_rev is not None: torev = self._reload_rev self._reload_rev = None try: tv.goto(torev) self.tableView_filelist.selectFile(self._reload_file) self._reload_file = None return except IndexError: pass wc = self.repo[None] idx = tv.model().index(0, 0) # Working directory or tip if not wc.dirty() and wc.p1().rev() >= 0: # parent of working directory is not nullrev _idx = tv.model().indexFromRev(wc.p1().rev()) # may appears if wc has been filtered out # (for example not on the filtered branch) if _idx is not None: idx = _idx tv.setCurrentIndex(idx) def revision_activated(self, rev=None): """ Callback called when a revision is double-clicked in the revisions table """ if rev is None: rev = self.tableView_revisions.current_rev self.toggleMainContent(True) self._manifestdlg = ManifestViewer(self.repo, rev) self._manifestdlg.show() def revision_selected(self, rev=None): """ Callback called when a revision is selected in the revisions table if rev == -1: refresh the current selected revision """ if not self.frame_maincontent.isVisible() or not self.repomodel.graph: return if rev == -1: view = self.tableView_revisions indexes = view.selectedIndexes() if not indexes: return rev = view.revFromindex(indexes[0]) ctx = self.repomodel.repo[rev] filename = self.tableView_filelist.currentFile() # save before refresh self.textview_status.setContext(ctx) if self.repomodel.show_hidden: self.textview_header.excluded = () else: self.textview_header.excluded = hiddenrevs(self.repo) self.textview_header.displayRevision(ctx) self.filelistmodel.setSelectedRev(ctx) if len(self.filelistmodel): if filename not in self.filelistmodel: filename = self.filelistmodel.file(0) self.tableView_filelist.selectFile(filename) self.tableView_filelist.file_selected[str].emit( filename) def goto(self, rev): if len(self.tableView_revisions.model().graph): self.tableView_revisions.goto(rev) else: # store rev to show once it's available (when graph # filling is still running) self._reload_rev = rev def _getrepomtime(self): """Return the last modification time for the repo""" watchedfiles = [(self.repo.root, ".hg", "store"), (self.repo.root, ".hg", "store", "00changelog.i"), (self.repo.root, ".hg", "dirstate"), (self.repo.root, ".hg", "store", "phasesroots"),] watchedfiles = [os.path.join(*wf) for wf in watchedfiles] for l in (self.repo.sjoin('lock'), self.repo.join('wlock')): try: if util.readlock(l): break except EnvironmentError, err: # depending on platform (win, nix) the "posix file" abstraction # defined and used by mercurial may raise one of both subclasses # of EnvironmentError if err.errno != errno.ENOENT: raise else: # repo not locked by an Hg operation mtime = [os.path.getmtime(wf) for wf in watchedfiles \ if os.path.exists(wf)] if mtime: return max(mtime) # humm, directory has probably been deleted, exiting... self.close() def ask_repository(self): repopath = QtGui.QFileDialog.getExistingDirectory( self, 'Select a mercurial repository', self.repo.root or os.path.expanduser('~')) repopath = find_repository(repopath) if not (repopath or self.repo.root): if not self.repo.root: raise RepoError("There is no Mercurial repository here (.hg not found)!") else: return None return repopath def open_repository(self, repopath=None): if not repopath: repopath = self.ask_repository() if repopath is None: return self.repo = build_repo(ui.ui(), repopath) self.setWindowTitle('hgview: %s' % os.path.abspath(self.repo.root)) self._finish_load() def reload(self): """Reload the repository""" self._reload_rev = self.tableView_revisions.current_rev self._reload_file = self.tableView_filelist.currentFile() self.repo = build_repo(self.repo.ui, self.repo.root) self._finish_load() def _finish_load(self): self._repodate = self._getrepomtime() self.setupBranchCombo() self.setupModels() self._setupQuickOpen() #@timeit def refreshRevisionTable(self, *args, **kw): """Starts the process of filling the HgModel""" branch = self.branch_comboBox.currentText() startrev = kw.get('rev', None) # XXX workaround: self.sender() may provoke a core dump if # this method is called directly (not via a connected signal); # the 'sender' keyword is a way to detect that the method # has been called directly (thus caller MUST set this kw arg) sender = kw.get('sender') or self.sender() follow = kw.get('follow', False) closed = self.branch_checkBox_action.isChecked() startrev = self.repo.changectx(startrev).rev() self.repomodel.show_hidden = self.get_action('showhide').isChecked() self.repomodel.setRepo(self.repo, branch=branch, fromhead=startrev, follow=follow, closed=closed) def on_about(self, *args): """ Display about dialog """ from hgviewlib.__pkginfo__ import modname, version, description try: from mercurial.version import get_version hgversion = get_version() except: from mercurial.__version__ import version as hgversion msg = "

About %(appname)s %(version)s

(using hg %(hgversion)s)" % \ {"appname": modname, "version": version, "hgversion": hgversion} msg += "

%s

" % description.capitalize() QtGui.QMessageBox.about(self, "About %s" % modname, msg) def on_help(self, *args): w = HgviewHelpViewer(self.repo, self) w.show() w.raise_() w.activateWindow() hgview-1.9.0/hgviewlib/hggraph.py0000644000015700001640000007045112607505500017561 0ustar narvalnarval00000000000000# Copyright (c) 2003-2012 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, see . """helper functions and classes to ease hg revision graph building Based on graphlog's algorithm, with inspiration stolen to TortoiseHg revision grapher. """ import os import re from cStringIO import StringIO import difflib from itertools import chain, count, imap from time import strftime, localtime from functools import partial import collections from mercurial.node import nullrev from mercurial import patch, util, match, error, hg import hgviewlib.hgpatches # force apply patches to mercurial from hgviewlib.hgpatches import mqsupport, phases, hiddenrevs from hgviewlib.util import (tounicode, isbfile, first_known_precursors,\ build_repo, allbranches) from hgviewlib.config import HgConfig DATE_FMT = '%F %R' # match the end of the diff header, assuming that the following line # looks like "@@ -25,6 +23,5 @@" DIFFHEADERMATCHER = re.compile('^@@.+@@$', re.MULTILINE) def diff(repo, ctx1, ctx2=None, files=None): """ Compute the diff of ``files`` between the 2 contexts ``ctx1`` and ``ctx2``. :Note: context may be a changectx or a filectx. * If ``ctx2`` is None, the parent of ``ctx1`` is used. If ``ctx1`` is a file ctx, the parent is the first ancestor that contains modification on the given file * If ``files`` is None, return the diff for all files. """ if not getattr(ctx1, 'applied', True): #no diff vs ctx2 on unapplied patch return ''.join(chain(ctx1.filectx(fname).data() for fname in files)) if ctx2 is None: ctx2 = ctx1.p1() if files is None: matchfn = match.always(repo.root, repo.getcwd()) else: matchfn = match.exact(repo.root, repo.getcwd(), files) # try/except for the sake of hg compatibility (API changes between # 1.0 and 1.1) diffopts = patch.diffopts(repo.ui) try: out = StringIO() patch.diff(repo, ctx2.node(), ctx1.node(), match=matchfn, fp=out, opts=diffopts) diffdata = out.getvalue() except: diffdata = '\n'.join(patch.diff(repo, ctx2.node(), ctx1.node(), match=matchfn, opts=diffopts)) return tounicode(diffdata) def __get_parents(repo, rev, subset, cache): """ Return non-null parents of `rev`. Only revision in subset are taken in account. """ # we need to manage our own stack or we overflow python one. toproceed = collections.deque() if rev not in cache: toproceed.append(rev) else: final = cache[rev] while toproceed: current = toproceed.pop() if current is None: parents = [x.rev() for x in repo.changectx(None).parents() if x] else: parents = [x for x in repo.changelog.parentrevs(current) if x != nullrev] final = [] added = set() for p in parents: if p in subset: final.append(p) added.add(p) else: if p not in cache: # stack the parent and jump to next iteration toproceed.append(current) toproceed.append(p) break else: for p in cache[p]: if p not in added: final.append(p) added.add(p) else: # all parents in cache register the new one cache[current] = final return final def getlog(model, ctx, gnode): if ctx.rev() is not None: msg = tounicode(ctx.description()) if msg: msg = msg.splitlines()[0] else: msg = u"WORKING DIRECTORY (locally modified)" return msg def gettags(model, ctx, gnode=None): if ctx.rev() is None: return u"" mqtags = ['qbase', 'qtip', 'qparent'] tags = ctx.tags() if model.hide_mq_tags: tags = [t for t in tags if t not in mqtags] return u",".join(imap(tounicode, tags)) def getdate(model, ctx, gnode): if not ctx.date(): return "" return strftime(DATE_FMT, localtime(int(ctx.date()[0]))) def ismerge(ctx): """ Return True if the changecontext ctx is a merge mode (should work with hg 1.0 and 1.2) """ if ctx: return len(ctx.parents()) == 2 and ctx.parents()[1] return False def _get_phaserevs(repo): """return phase information for all rev in repo this function is useful to stay compatible with multiple Mercurial version if the version does not support phases the function return None""" phaserevs = None if getattr(repo, '_phasecache', None) is not None: phaserevs = repo._phasecache._phaserevs elif getattr(repo, '_phaserev', None) is not None: phaserevs = repo._phaserev return phaserevs def _dirty_wc(repo): """return true if the working directory has changes""" return bool(sum(len(l) for l in repo.status())) # dirty working dir def _rev_order(phaserevs, rev): """return a sort key for changeset reordering (, rev) Freshness is: :0: public revision :1: mutable revision :42: working directory """ if rev is None: return (42, rev) return (phases.public != phaserevs[rev], rev) def _build_filter(start_rev, branch, follow, closed): """process filter rules and returns maching revisions as list If start_rev is None, started from working directory node If follow is True, only generated the subtree from the start_rev head. If branch is set, only generated the subtree for the given named branch. """ # selected range revset_args = [] if (start_rev is None): revset = 'all()' elif follow: revset = '::(%s)' % start_rev else: revset = ':(%s)' % start_rev # branch restriction if branch: # we add both changeset that belong to the branch and other merged. revset = '(%s and (branch(%%s) or parents(branch(%%s))))' % (revset) revset_args.extend([branch, branch]) # closed branch restriction if not closed: revset = '(%s and ::(head() - closed()))' % revset return revset, revset_args def _revset_revs(repo, revset, revset_args=(), view='visible'): """Use the fastest available method to get revs from a revset The view argument is used to control the available revision (see `repoview` in Mercurila core). We currently support "visible" and None as we have to implement backward compat manually. The revs are returned in ordered.""" revset = 'sort(%s)' % revset assert view in ('visible', None) if getattr(repo, 'filtered', None) is not None: # Mercurial 2.5 and above. repoview available, relying on it. if view is None: repo = repo.unfiltered() else: repo = repo.filtered(view) return repo.revs(revset, *revset_args) else: if getattr(repo, 'revs', None) is None: # pre 2.1 hg revs = [c.rev() for c in repo.set(revset, *revset_args)] else: revs = repo.revs(revset, *revset_args) excluded = () if view is None else hiddenrevs(repo) if excluded: revs = [r for r in revs if r not in excluded] return revs def revision_grapher(repo, revset_filter, start_rev=None, show_hidden=False, reorder=False, show_obsolete=False): """incremental revision grapher This generator function walks through the revision history from revision start_rev for each revision emits tuples with the following elements: - current revision - column of the current node in the set of ongoing edges - color of the node (?) - lines; a list of (col, next_col, color) indicating the edges between the current row and the next row - parent revisions of current revision """ # special handling of mq patches if show_hidden and start_rev == None and hasattr(repo, 'mq'): series = list(reversed(repo.mq.series)) for patchname in series: if not repo.mq.isapplied(patchname): yield (patchname, 0, 0, [(0, 0 ,0, False)], []) view = None if show_hidden else 'visible' included = _revset_revs(repo, *revset_filter, view=view) if getattr(included, 'append', None) is None: # _revset_revs returned a smartset (new in Mercurial 3.0) # translate it back to list for now included = list(included) # working directory if start_rev is None and _dirty_wc(repo): included.append(None) # perform reordering phaserevs = _get_phaserevs(repo) if not (show_hidden or phaserevs is None or not reorder): keyfunc = partial(_rev_order, phaserevs) included.sort(key=keyfunc) included.reverse() inc_set = set(included) # necessary for first_known_precursors # the second user of first_known_precursors can't really # reverse its logic. excluded = set(repo) - inc_set parentscache = {} # all known revs for this line. This is used to compute column index # it's combined with next_revs to compute how we must draw lines revs = [] levels = [] # a rev -> level mapping. # level are True for real relation (parent), # False for weak one (obsolete) rev_color = {} free_color = count(0) parent_func = partial(__get_parents, repo=repo, subset=inc_set, cache=parentscache) for curr_rev in included: # Compute revs and next_revs. if curr_rev not in revs: # rev not ancestor of already processed node # we add this new head to know revision revs.append(curr_rev) levels.append(True) rev_color[curr_rev] = curcolor = free_color.next() else: curcolor = rev_color[curr_rev] # copy known revisions for this line next_revs = revs[:] next_levels = levels[:] # Add parents to next_revs. parents = [(p, True) for p in parent_func(rev=curr_rev)] if show_obsolete: ctx = repo[curr_rev] for prec in first_known_precursors(ctx, excluded): parents.append((prec.rev(), False)) parents_to_add = [] max_levels = dict(zip(next_revs, next_levels)) for idx, (parent, level) in enumerate(parents): # could have been added by another children if parent not in next_revs: parents_to_add.append(parent) if idx == 0: # first parent inherit the color rev_color[parent] = curcolor else: # second don't rev_color[parent] = free_color.next() max_levels[parent] = level or max_levels.get(parent, False) # rev_index is also the column index rev_index = next_revs.index(curr_rev) # replace curr_rev by its parents. next_revs[rev_index:rev_index + 1] = parents_to_add next_levels = [max_levels[r] for r in next_revs] lines = [] for i, rev in enumerate(revs): if rev == curr_rev: # one or more line to parents targets = parents else: # single line to the same rev targets = [(rev, levels[i])] for trg, level in targets: color = rev_color[trg] lines.append((i, next_revs.index(trg), color, level)) yield (curr_rev, rev_index, curcolor, lines, parents) revs = next_revs levels = next_levels def filelog_grapher(repo, path): ''' Graph the ancestry of a single file (log). Deletions show up as breaks in the graph. ''' filerev = len(repo.file(path)) - 1 fctx = repo.filectx(path, fileid=filerev) rev = fctx.rev() flog = fctx.filelog() heads = [repo.filectx(path, fileid=flog.rev(x)).rev() for x in flog.heads()] assert rev in heads heads.remove(rev) revs = [] rev_color = {} nextcolor = 0 _paths = {} while rev >= 0: # Compute revs and next_revs if rev not in revs: revs.append(rev) rev_color[rev] = nextcolor ; nextcolor += 1 curcolor = rev_color[rev] index = revs.index(rev) next_revs = revs[:] # Add parents to next_revs fctx = repo.filectx(_paths.get(rev, path), changeid=rev) for pfctx in fctx.parents(): _paths[pfctx.rev()] = pfctx.path() parents = [pfctx.rev() for pfctx in fctx.parents()]# if f.path() == path] parents_to_add = [] for parent in parents: if parent not in next_revs: parents_to_add.append(parent) if len(parents) > 1: rev_color[parent] = nextcolor ; nextcolor += 1 else: rev_color[parent] = curcolor parents_to_add.sort() next_revs[index:index + 1] = parents_to_add lines = [] for i, nrev in enumerate(revs): if nrev in next_revs: color = rev_color[nrev] lines.append( (i, next_revs.index(nrev), color, True) ) elif nrev == rev: for parent in parents: color = rev_color[parent] lines.append( (i, next_revs.index(parent), color, True) ) pcrevs = [pfc.rev() for pfc in fctx.parents()] yield (fctx.rev(), index, curcolor, lines, pcrevs, _paths.get(fctx.rev(), path)) revs = next_revs if revs: rev = max(revs) else: rev = -1 if heads and rev <= heads[-1]: rev = heads.pop() class GraphNode(object): """ Simple class to encapsulate e hg node in the revision graph. Does nothing but declaring attributes. """ def __init__(self, rev, xposition, color, lines, parents, ncols=None, extra=None): self.rev = rev self.x = xposition self.color = color if ncols is None: ncols = len(lines) self.cols = ncols if not parents: self.cols += 1 # root misses its own column self.parents = parents self.bottomlines = lines self.toplines = [] self.extra = extra class Graph(object): """ Graph object to ease hg repo navigation. The Graph object instantiate a `revision_grapher` generator, and provide a `fill` method to build the graph progressively. """ #@timeit def __init__(self, repo, grapher, maxfilesize=100000): self.maxfilesize = maxfilesize self.repo = repo self.maxlog = len(self.repo.changelog) self.grapher = grapher self.nodes = [] self.nodesdict = {} self.max_cols = 0 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 stopped = False mcol = [self.max_cols] for vnext in self.grapher: if vnext is None: continue nrev, xpos, color, lines, parents = vnext[:5] if isinstance(nrev, int) and nrev >= self.maxlog: continue gnode = GraphNode(nrev, xpos, color, lines, parents, extra=vnext[5:]) if self.nodes: gnode.toplines = self.nodes[-1].bottomlines self.nodes.append(gnode) self.nodesdict[nrev] = gnode mcol.append(gnode.cols) if rev is not None and nrev <= rev: rev = None # we reached rev, switching to nnodes counter if rev is None: if nnodes is not None: nnodes -= 1 if not nnodes: break else: break else: self.grapher = None stopped = True self.max_cols = max(mcol) return not stopped def isfilled(self): return self.grapher is None def fill(self, step=100): """ Return a generator that fills the graph by bursts of `step` more nodes at each iteration. """ while self.build_nodes(step): yield len(self) yield len(self) 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 index(self, rev): if len(self) == 0: # graph is empty, let's build some nodes self.build_nodes(10) if rev is not None and rev < self.nodes[-1].rev: self.build_nodes(self.nodes[-1].rev - rev) if rev in self.nodesdict: return self.nodes.index(self.nodesdict[rev]) return -1 def fileflags(self, filename, rev, _cache={}): """ Return a couple of flags ('=', '+', '-' or '?') depending on the nature of the diff for filename between rev and its parents. """ if rev not in _cache: ctx = self.repo.changectx(rev) _cache.clear() _cache[rev] = (ctx, [self.repo.status(p.node(), ctx.node())[:5] for p in ctx.parents()]) ctx, allchanges = _cache[rev] flags = [] for changes in allchanges: # changes = modified, added, removed, deleted, unknown for flag, lst in zip(["=", "+", "-", "-", "?"], changes): if filename in lst: if flag == "+": renamed = ctx.filectx(filename).renamed() if renamed: flags.append(renamed) break flags.append(flag) break else: flags.append('') return flags def fileflag(self, filename, rev): """ Return a flag (see fileflags) between rev and its first parent (may be long) """ return self.fileflags(filename, rev)[0] def filename(self, rev): return self.nodesdict[rev].extra[0] def filedata(self, filename, rev, mode='diff', flag=None, maxfilesize=None): """XXX written under dubious encoding assumptions The modification flag is computed using *fileflag* if ``flag`` is None. """ # XXX This really begins to be a dirty mess... if maxfilesize is None: maxfilesize = self.maxfilesize data = "" if flag is None: flag = self.fileflag(filename, rev) ctx = self.repo.changectx(rev) filesize = 0 try: fctx = ctx.filectx(filename) filesize = fctx.size() # compute size here to lookup data securely except (LookupError, OSError): fctx = None # may happen for renamed/removed files or mq patch ? if isbfile(filename): data = u"[bfile]\n" if fctx: data = fctx.data() data += u"footprint: %s\n" % tounicode(data) return "+", data if flag not in ('-', '?'): if fctx is None:# or fctx.node() is None: return '', None if maxfilesize >= 0 and filesize > maxfilesize: try: div = int(filesize).bit_length() // 10 sym = ('', 'K', 'M', 'G', 'T', 'E')[div] # more, really ??? val = int(filesize / (2 ** (div * 10))) except AttributeError: # py < 2.7 val = filesize sym = '' data = u"~%i%so" % (val, sym) return 'file too big', data if flag == "+" or mode == 'file': flag = '+' # return the whole file data = fctx.data() if util.binary(data): data = u"binary file" else: # tries to convert to unicode data = tounicode(data) elif flag == "=" or isinstance(mode, int): flag = "=" if isinstance(mode, int): parentctx = self.repo.changectx(mode) else: parentctx = self.repo[self._fileparent(fctx)] data = diff(self.repo, ctx, parentctx, files=[filename]) match = DIFFHEADERMATCHER.search(data) if match is not None: datastart = match.start() else: datastart = 0 data = data[datastart:] elif flag == '': data = u'' else: # file renamed oldname, node = flag newdata = fctx.data().splitlines() olddata = self.repo.filectx(oldname, fileid=node) olddata = olddata.data().splitlines() data = list(difflib.unified_diff(olddata, newdata, oldname, filename))[2:] if data: flag = "=" else: data = newdata flag = "+" data = u'\n'.join(tounicode(elt) for elt in data) return flag, data def _fileparent(self, fctx): try: return fctx.p1().rev() except IndexError: # reach bottom return -1 def fileparent(self, filename, rev): return self._fileparent(self.repo[rev].filectx(filename)) class HgRepoListWalker(object): """ Graph object to ease hg repo revision tree drawing depending on user's configurations. """ _allcolumns = ('ID', 'Branch', 'Log', 'Author', 'Date', 'Tags',) _columns = ('ID', 'Branch', 'Log', 'Author', 'Date', 'Tags', 'Bookmarks') _stretchs = {'Log': 1, } _getcolumns = "getChangelogColumns" def __init__(self, repo, branch='', fromhead=None, follow=False, closed=False, parent=None, *args, **kwargs): """ repo is a hg repo instance """ #XXX col radius self._datacache = {} self._hasmq = False self.mqueues = [] self.wd_revs = [] self.graph = None self.rowcount = 0 self.repo = repo self.show_hidden = False self.load_config() self.setRepo(repo, branch=branch, fromhead=fromhead, follow=follow, closed=closed) def setRepo(self, repo=None, branch='', fromhead=None, follow=False, closed=False): if repo is None: repo = build_repo(self.repo.ui, self.repo.root) self._hasmq = hasattr(self.repo, "mq") if not getattr(repo, '__hgview__', False) and self._hasmq: mqsupport.reposetup(repo.ui, repo) oldrepo = self.repo self.repo = repo if oldrepo.root != repo.root: self.load_config() self._datacache = {} try: wdctxs = self.repo.changectx(None).parents() except error.Abort: # might occur if reloading during a mq operation (or # whatever operation playing with hg history) return if self._hasmq: self.mqueues = self.repo.mq.series[:] self.wd_revs = [ctx.rev() for ctx in wdctxs] self.wd_status = [self.repo.status(ctx.node(), None)[:4] for ctx in wdctxs] self._user_colors = {} self._branch_colors = {} # precompute named branch color for stable value. for branch_name in chain(['default', 'stable'], sorted(allbranches(repo, True))): self.namedbranch_color(branch_name) revset_filter = _build_filter(fromhead, branch, follow, closed) grapher = revision_grapher(self.repo, revset_filter, start_rev=fromhead, show_hidden=self.show_hidden, reorder=self.reorder_changesets, show_obsolete=self.show_obsolete) self.graph = Graph(self.repo, grapher, self.max_file_size) self.rowcount = 0 self.heads = [self.repo.changectx(x).rev() for x in self.repo.heads()] self.ensureBuilt(row=self.fill_step) def ensureBuilt(self, rev=None, row=None): """ Make sure rev data is available (graph element created). """ if self.graph.isfilled(): return required = 0 buildrev = rev n = len(self.graph) if rev is not None: if n and self.graph[-1].rev <= rev: buildrev = None else: required = self.fill_step / 2 elif row is not None and row > (n - self.fill_step / 2): required = row - n + self.fill_step if required or buildrev: self.graph.build_nodes(nnodes=required, rev=buildrev) self.updateRowCount() elif row and row > self.rowcount: # asked row was already built, but views where not aware of this self.updateRowCount() elif rev is not None and rev <= self.graph[self.rowcount].rev: # asked rev was already built, but views where not aware of this self.updateRowCount() def updateRowCount(self): self.rowcount = None #raise NotImplementedError def rowCount(self, parent=None): return self.rowcount def columnCount(self, parent=None): return len(self._columns) def load_config(self): cfg = HgConfig(self.repo.ui) self._users, self._aliases = cfg.getUsers() self.dot_radius = cfg.getDotRadius(default=8) self.rowheight = cfg.getRowHeight() self.fill_step = cfg.getFillingStep() self.max_file_size = cfg.getMaxFileSize() self.hide_mq_tags = cfg.getMQHideTags() self.show_hidden = cfg.getShowHidden() self.reorder_changesets = cfg.getNonPublicOnTop() self.show_obsolete = cfg.getShowObsolete() cols = getattr(cfg, self._getcolumns)() if cols is not None: validcols = [col for col in cols if col in self._allcolumns] if len(validcols) == len(cols) and \ set(['Log', 'ID']).issubset(set(validcols)) : self._columns = tuple(validcols) @staticmethod def get_color(n, ignore=()): return [] def user_color(self, user): if user not in self._user_colors: self._user_colors[user] = self.get_color(len(self._user_colors), self._user_colors.values()) return self._user_colors[user] def user_name(self, user): return self._aliases.get(user, user) def namedbranch_color(self, branch): if branch not in self._branch_colors: self._branch_colors[branch] = self.get_color(len(self._branch_colors)) return self._branch_colors[branch] def col2x(self, col): return (1.2*self.dot_radius + 0) * col + self.dot_radius/2 + 3 def rowFromRev(self, rev): row = self.graph.index(rev) if row == -1: row = None return row def clear(self): """empty the list""" self.graph = None self._datacache = {} self.notify_data_changed() def notify_data_changed(self): pass hgview-1.9.0/hgviewlib/hgpatches/0000775000015700001640000000000012607506603017535 5ustar narvalnarval00000000000000hgview-1.9.0/hgviewlib/hgpatches/graphmod.py0000644000015700001640000000275412607505500021711 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012 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, see . ''' Contains Hg compatibility for older versions ''' #pylint: disable=E0611 try: from mercurial import graphmod if not getattr(graphmod, '_fixlongrightedges', None): # because of lazy import raise ImportError from mercurial.graphmod import (_fixlongrightedges, _getnodelineedgestail, _drawedges, _getpaddingline) except ImportError: # <2.3 from hgext.graphlog import (fix_long_right_edges as _fixlongrightedges, get_nodeline_edges_tail as _getnodelineedgestail, draw_edges as _drawedges, get_padding_line as _getpaddingline) hgview-1.9.0/hgviewlib/hgpatches/mqsupport.py0000644000015700001640000002562212607505500022161 0ustar narvalnarval00000000000000# Copyright (c) 2003-2012 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, see . """ The main goal of this module is to create fake mercurial change context classes from data information available in mq patch files. Only methods that are required by hgview had been implemented. They may have special features to help hgview, So use it with care. The main differences are: * node, rev, hex are all strings (patch name) * files within patches are always displayed as modified files * manifest only shows files modified by the mq patch. * data may be empty (date, description, status, tags, branch, etc.) * the parent of a patch may by the last applied on or previous patch or nullid * the child of a patch is the next patch * patches are hidden """ from __future__ import with_statement import re import os import os.path as osp from operator import or_ from itertools import chain from mercurial import error, node, patch, context, manifest from hgext.mq import patchheader from hgviewlib.hgpatches import phases MODIFY, ADD, REMOVE, DELETE, UNKNOWN, RENAME = range(6) # order is important for status class PatchMetaData(object): def __init__(self, path, oldpath, op): self.path = path self.oldpath = oldpath self.op = op class MqLookupError(error.LookupError): """Specific exception dedicated to mq patches""" class MqCtx(context.changectx): """Base class of mq patch context (changectx, filectx, etc.)""" def __init__(self, repo, patch_name): self.name = patch_name self._rev = self.name self._repo = repo self._queue = self._repo.mq self.path = self._queue.join(self.name) @property def applied(self): return bool(self._queue.isapplied(self.name)) def __contains__(self, filename): return filename in self.files() def __iter__(self): for filename in self.files(): yield filename def __getitem__(self, filename): return self.filectx(filename) def files(self): """Return the list of related files""" raise NotImplementedError def filectx(self, path, **kwargs): """Return the context related to the filename""" raise NotImplementedError class MqChangeCtx(MqCtx): """ A Mercurial change context fake for unapplied mq patch. Use with care as methods may be missing or have special features. """ def __init__(self, repo, patch_name): super(MqChangeCtx, self).__init__(repo, patch_name) if patch_name is None: raise ValueError self._header_cache = None self._diffs_cache = None self._files_cache = None def __repr__(self): return '' % self.name @property def _header(self): if self._header_cache is not None: return self._header_cache self._header_cache = patchheader(self.path) return self._header_cache @property def _diffs(self): # cache on first access only to speed up the process if self._diffs_cache is not None: return self._diffs_cache self._diffs_cache = [] hunks = None meta = None data = None with open(self.path) as fid: for event, data in patch.iterhunks(fid): if event == 'file': if hunks: self._diffs_cache.append(MqFileCtx(hunks, meta, self)) hunks = [] meta = data[-1] if not hasattr(meta, 'path'): new, old = data[:2] meta = PatchMetaData(new[2:], old[2:], 'UNKNOWN') # [2:] = remove a/ elif event == 'hunk' and data: hunks.append(data) if hunks: self._diffs_cache.append(MqFileCtx(hunks, meta, self)) return self._diffs_cache def branch(self): return getattr(self._header, 'branch', '') def children(self): series = self._queue.series try: idx = series.index(self.name) return [self._repo.changectx(series[idx + 1]) if idx else self._repo[None]] except IndexError: return [self._repo[node.nullid]] def date(self): date = self._header.date if not date: return () date, timezone = date.split() return float(date), int(timezone) def description(self): return '\n'.join(self._header.message) def filectx(self, filename, _cache=[], **kwargs): for diff in self._diffs: if diff.path == filename: return diff raise MqLookupError(self.name, filename, 'file not in manifest.') def files(self): if self._files_cache is not None: return self._files_cache out = list(set(chain(*(diff.files() for diff in self._diffs)))) self._files_cache = out return out def flags(self, path): return '' def hex(self): return self.name def hidden(self): return True def phase(self): return phases.secret def manifest(self): return manifest.manifestdict.fromkeys(self.files(), '=') def node(self): '''Return the name of the patch''' return self.name @property def _node(self): return self.node() # in that way to support old hg def parents(self): if self._header.parent: try: return [self._repo[self._header.parent]] except error.RepoLookupError: pass series = self._queue.series if not self.name in series: return [] idx = series.index(self.name) return [self._repo.changectx(series[idx - 1]) if idx else self._repo[None]] def rev(self): return self.name def status(self): return () def tags(self): return [self.name] def user(self): return self._header.user or '' class _MqMissingPatch_Header(object): """Patch header fake for missing file""" message = (':ERROR: patch file is missing !!!',) date, breanch, user, parent = ('',) * 4 class MqMissingChangeCtx(MqChangeCtx): """Changeset class for patch in series without file.""" def __init__(self, repo, patch_name): super(MqMissingChangeCtx, self).__init__(repo, patch_name) self._header_cache = _MqMissingPatch_Header() self._diffs_cache = () self._files_cache = () def __repr__(self): return '' % self.name class MqFileCtx(context.filectx): """Mq Fake for file context""" def __init__(self, hunks, meta, changectx): self._changectx = changectx self._repo = changectx._repo self._path = meta.path self._oldpath = meta.oldpath self._operation = meta.op self._data = '\n\n\n' self._data += ''.join(l for h in hunks for l in h.hunk if h) # XXX how to deal diff encodings? try: self._data = unicode(self._data, "utf-8") except UnicodeError: # XXX use a default encoding from config? self._data = unicode(self._data, "iso-8859-15", 'ignore') @property def path(self): return self._path @property def oldpath(self): return self._oldpath def files(self): """List of modified files""" return tuple(path for path in (self._path, self._oldpath) if path and not os.devnull.endswith(path)) # note endswith is used as the complete path have been cut # (expecting ``a/`` at the beginning of path) def data(self): """ return the patch hunks""" return self._data __str__ = data def isexec(self): return False # XXX def __repr__(self): return ('' % (self._path, self._changectx.name)) def flags(self): return '' def renamed(self): if self.state == 'RENAME': return self._oldpath, self._path return False def parents(self): try: return [self._changectx._repo[self._changectx._header.parent].filectx(self.path)] except error.RepoLookupError: return [self] def size(self): return len(self._data) @property def state(self): return self._operation or 'UNKNOWN' def filelog(self): return None # ___________________________________________________________________________ def reposetup(ui, repo): """ extend repo class with special mq logic """ if (not repo.local()) or (not hasattr(repo, "mq")): return repo.unapplieds = filter(repo.mq.unapplied, repo.mq.series) getitem_orig = repo.__getitem__ status_orig = repo.status lookup_orig = repo.lookup class MqRepository(repo.__class__): __hgview__ = True def __getitem__(self, changeid): if changeid not in self.unapplieds: #pylint: disable=E1101 return getitem_orig(changeid) patch = MqChangeCtx(repo, changeid) if os.path.exists(patch.path): return patch return MqMissingChangeCtx(repo, changeid) def status(self, node1='.', node2=None, match=None, *args, **kwargs): if isinstance(node1, context.changectx): ctx1 = node1 else: ctx1 = self[node1] if isinstance(node2, context.changectx): ctx2 = node2 else: ctx2 = self[node2] if not (isinstance(ctx1, MqCtx) or isinstance(ctx2, MqCtx)): return super(MqRepository, self).status(ctx1, ctx2, match, *args, **kwargs) # modified, added, removed, deleted, unknown status = ([], [], [], [], [], [], []) if match is None: match = lambda x: x # force patch content as MODIFY which is close to what a patch is :D status[MODIFY][:] = [path for path in ctx2.files() if match(path)] return status def lookup(self, key): if isinstance(key, MqCtx): return key.node() if key in repo.unapplieds: return key return lookup_orig(key) # common way for hg extensions repo.__class__ = MqRepository hgview-1.9.0/hgviewlib/hgpatches/__init__.py0000644000015700001640000001172512607505500021645 0ustar narvalnarval00000000000000# Copyright (c) 2003-2012 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, see . """ This modules contains monkey patches for Mercurial allowing hgview to support older versions """ from functools import partial from mercurial import changelog, filelog, patch, context, localrepo from mercurial import demandimport # for CPython > 2.7 (see pkg_resources) module [loaded by pygments]) demandimport.ignore.append("_frozen_importlib") # from pyinotify which try from functools import reduce demandimport.ignore.append("functools") if not hasattr(changelog.changelog, '__len__'): changelog.changelog.__len__ = changelog.changelog.count if not hasattr(filelog.filelog, '__len__'): filelog.filelog.__len__ = filelog.filelog.count # mercurial ~< 1.8.4 if patch.iterhunks.func_code.co_varnames[0] == 'ui': iterhunks_orig = patch.iterhunks ui = type('UI', (), {'debug':lambda *x: None})() iterhunks = partial(iterhunks_orig, ui) patch.iterhunks = iterhunks # mercurial ~< 1.8.3 if not hasattr(context.filectx, 'p1'): context.filectx.p1 = lambda self: self.parents()[0] # mercurial < 2.1 if not hasattr(context.changectx, 'phase'): from hgviewlib.hgpatches.phases import phasenames context.changectx.phase = lambda self: 0 context.changectx.phasestr = lambda self: phasenames[self.phase()] context.workingctx.phase = lambda self: 1 # note: use dir(...) has localrepo.localrepository.hiddenrevs always raises # an attribute error - because the repo is not set yet if 'hiddenrevs' not in dir(localrepo.localrepository): def hiddenrevs(self): return getattr(self.changelog, 'hiddenrevs', ()) localrepo.localrepository.hiddenrevs = property(hiddenrevs, None, None) try: from mercurial.repoview import filterrevs def hiddenrevs(repo): return filterrevs(repo, 'visible') except ImportError: def hiddenrevs(repo): # mercurial < 2.5 has no filteredrevs # mercurial < 2.3 has hiddenrevs on changelog # mercurial < 1.9 has no hiddenrevs return getattr(repo, 'hiddenrevs', getattr(repo.changelog, 'hiddenrevs', ())) # obsolete feature if getattr(context.changectx, 'obsolete', None) is None: context.changectx.obsolete = lambda self: False if getattr(context.changectx, 'unstable', None) is None: context.changectx.unstable = lambda self: False ### unofficial API implemented by mutable extension # They will probably because official, but maybe with a different name has_conflicting = True if getattr(context.changectx, 'conflicting', None) is None: has_conflicting = False context.changectx.conflicting = lambda self: False if getattr(context.changectx, 'divergent', None) is None: if has_conflicting: # older version with real conflicting support. rely on this for divergent. context.changectx.divergent = lambda self: self.conflicting() else: context.changectx.divergent = lambda self: False has_latecomer = True if getattr(context.changectx, 'latecomer', None) is None: has_latecomer = False context.changectx.latecomer = lambda self: self.bumped() if getattr(context.changectx, 'bumped', None) is None: if has_latecomer: # older version with real latecomer support. rely on this for bumped. context.changectx.bumped = lambda self: self.latecomer() else: context.changectx.bumped = lambda self: False if getattr(context.changectx, 'troubles', None) is None: def troubles(ctx): troubles = [] if ctx.unstable(): troubles.append('unstable') if ctx.bumped(): # rename troubles.append('bumped') if ctx.divergent(): troubles.append('divergent') return tuple(troubles) context.changectx.troubles = troubles try: # meaning of obsstore attribute have been flipped between mercurial 2.3 and # mercurial 2.4 import mercurial.obsolete mercurial.obsolete.getrevs except (ImportError, AttributeError): # pre Mercurial 2.4 def precursorsmarkers(obsstore, node): return obsstore.successors.get(node, ()) def successorsmarkers(obsstore, node): return obsstore.precursors.get(node, ()) else: def precursorsmarkers(obsstore, node): return obsstore.precursors.get(node, ()) def successorsmarkers(obsstore, node): return obsstore.successors.get(node, ()) hgview-1.9.0/hgviewlib/hgpatches/scmutil.py0000644000015700001640000000211412607505500021556 0ustar narvalnarval00000000000000# Copyright (c) 2003-2012 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, see . """ """ try: from mercurial.scmutil import match #pylint: disable=E1101 except ImportError: from mercurial import cmdutil def match(ctx, *args, **kwargs): return cmdutil.match(ctx._repo, *args, **kwargs) try: from mercurial.scmutil import revrange except ImportError: revrange = lambda repo, rev: [repo.changectx(rev).rev()] hgview-1.9.0/hgviewlib/hgpatches/phases.py0000644000015700001640000000165212607505500021367 0ustar narvalnarval00000000000000# Copyright (c) 2003-2012 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, see . """ add a phases mock to mercurial """ try: from mercurial.phases import * except ImportError: allphases = public, draft, secret = range(3) phasenames = ['public', 'draft', 'secret'] hgview-1.9.0/hgviewlib/inotify.py0000644000015700001640000001100612607505500017611 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ inotify support for hgview """ from os import read, path as osp from time import sleep from array import array from fcntl import ioctl from termios import FIONREAD from struct import unpack, calcsize from pyinotify import WatchManager class Inotify(object): """Use inotify to get a file descriptor that shall be used into a main loop. Constructor arguments: * repo - a mercurial repository object to watch * callback - callable called while processing events Use the ``process()`` method to update the display """ def __init__(self, repo, callback=None): self.watchmanager = WatchManager() self._fd = self.watchmanager.get_fd() self.repo = repo self.callback = callback def update(self): '''update watchers''' # sorry :P. Import them here to reduce stating time from pyinotify import (IN_MODIFY, IN_ATTRIB, IN_MOVED_FROM, IN_MOVED_TO, IN_DELETE_SELF, IN_MOVE_SELF, ALL_EVENTS, IN_CLOSE_WRITE, IN_CREATE, IN_DELETE) mask = (IN_MODIFY | IN_ATTRIB | IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE_SELF | IN_MOVE_SELF | IN_CLOSE_WRITE | IN_CREATE | IN_DELETE) self.watchmanager.add_watch(self.repo.root, mask, rec=True, auto_add=True,) def get_fd(self): """Return assigned inotify's file descriptor.""" return self.watchmanager.get_fd() def read_events(self): """ Read events and return related file name. """ buf_ = array('i', [0]) # get event queue size if ioctl(self._fd, FIONREAD, buf_, 1) == -1: return queue_size = buf_[0] try: # Read content from file raw = read(self._fd, queue_size) except Exception, msg: raise NotifierError(msg) rsum = 0 # counter data_fmt = 'iIII' data_size = calcsize(data_fmt) while rsum < queue_size: # Retrieve wd, mask, cookie and fname_len wd, mask, cookie, fname_len = unpack(data_fmt, raw[rsum:rsum + data_size]) # Retrieve name fname, = unpack('%ds' % fname_len, raw[rsum + data_size:rsum + data_size + fname_len]) end = fname.find('\x00') if end != -1: fname = fname[:end] rsum += data_size + fname_len yield fname def process(self): '''process events''' # Many events are raised for each modification on the repository # files or history (many files processed while committing, each file # processing may raises many event, e.g.IN_MODIFY and IN_ATTRIB. # We don't have to update the repository at each event. So, it may be a # good idea to put a delay during which the events are consumed, before # processing the callback. # Note: not implemented here, use the application mainloop to do so. # Note: I've try some other solutions, for instance: watching for # .hg/wlock or focusing on the manifests). But it seems that they # require a much more complicated implementation (update watched # files, fine watchers handling). # Finally, the current solution is simple, robust, and the end-user # interface seems to be good enough. # .hg/wlock means that some process is currently running on the # repository, so we have to sleep more. We can just return as another # event will be sent. if osp.exists(osp.join(self.repo.root or '', '.hg', 'wlock')): return # refresh viewer if self.callback: self.callback() hgview-1.9.0/hgviewlib/hgviewhelp.py0000644000015700001640000001713312607505500020301 0ustar narvalnarval00000000000000# Copyright (c) 2003-2012 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, see . """ help messages for hgview """ help_msg = """ hgview: a visual hg log viewer ============================== hgview without any parameters will launch the hgview log navigator, allowing to visually browse the hg graph log, search in logs, and display diff between arbitrary revisions of a file, with simple support for mq and bigfile extensions. If a filename is given the filelog diff viewer is launched for this file, and with the '-n' option the filelog navigator is launched for the file. With the '-r' option the manifest viewer is launched for the given revision. Revision graph browser ---------------------- The main revision graph displays the repository history as a graph, sorted by revision number. Hit Space key or click the button to show or hide the changeset content. The color of the node of each revision depends on the named branch the revision belongs to. The color of the links (between nodes) is randomly chosen. The position of the working directory is marked on the graph with a red border around the node marker. If the working directory has local modifications, a virtual 'WORKING DIRECTORY' revision is added in the graph with a warning icon (with no revision number). Modified, added and removed files are listed and browsable as a normal changeset node. Note that if the working directory is in merge state, there will be 2 revisions marked as modified in the graph (since the working directory is then a child of both the merged nodes). mq support ~~~~~~~~~~ There is a simple support for the mq extension. Applied patches are seen in the revlog graph with a special arrow icon. Unapplied patches are not changesets and are not shown in the revlog graph. Instead, when the currently selected revision is an applied patch, the revision metadata display (see below) area shows an additional 'Patch queue' line with colored background listing all available patches, applied or not. The content of unapplied patches cannot be shown, but it will be indicated that there are other unapplied patches if there is at least one applied patch). The current patch is bold and unapplied patches are italic. Revision metadata display ------------------------- Parents, ancestors and children of the current changeset are showed with two kinds of links: - clicking the **changeset ID** (hash value) navigates to the given revision, - clicking the **revision number** (integer value) of parents and ancestors changes which other revision is used to compute the diff of merges. This allows you to compare the merged node with each of its parents, or even with the common ancestor of these 2 nodes. The currently selected ancestor is shown bold. Revision description rendering ------------------------------ The revision's description text is interpreted as ReStructuredText. Commit messages can thus contain formatted text, links, tables, references, etc. RST links without scheme will link to the specified revision (changeset ID/revision number/tag name). For instance:: `links in fancy view open browser `_ Revisions modified file list ----------------------------- The file list displays the list of files modified in the current revision. The diff view shows the diff of the currently selected modified file and the currently selected ancestor. On a merge node, by default, only files which are different from both its parents are listed here. However, you can display the list of all modified files by double-clicking the file list column header. Quickbars --------- Quickbars are toolbars that appear at the bottom of the screen. Only one quickbar can be displayed at a time. When a quickbar is visible, hitting the Esc key makes it disappear. The goto quickbar ~~~~~~~~~~~~~~~~~ This toolbar appears when hitting Ctrl+G. It allows you to jump to a given revision. The destination revision can be entered by: - it's revision number (negative values allowed, counting back from tip=-1) - it's changeset ID (short or long) - a tag name (partial or full) - a branch name - an empty string - means the current parent of the working directory The search quickbar ~~~~~~~~~~~~~~~~~~~ This toolbar appears when hitting Ctrl+F or / (if not in goto toolbar). It allows you to type a string to be searched for: - in the currently displayed revision commit message (with highlight-as-you-type) - in the currently displayed file or diff (with highlight-as-you-type) Hitting the "Search next" button starts a background task for searching through the whole revision log, starting from the currently selected revision and file. Keyboard shortcuts ------------------ **Up/Down** go to next/previous revision **Middle Click** go to the common ancestor of the clicked revision and the currently selected one **Left/Right** display previous/next files of the current changeset **Ctrl+F** or **/** display the search 'quickbar' **Ctrl+G** display the goto 'quickbar' **Esc** exit hgview or hide the visible 'quickbar' **Enter** run the diff viewer for the currently selected file (display diff between revisions) **Alt+Enter** run the filelog navigator **Shift+Enter** run the manifest viewer for the displayed revision **Ctrl+R** reload repository; should happen automatically if it is modified outside hgview (due to a commit, a pull, etc.) **Alt+Up/Down** display previous/next diff block **Alt+Left/Right** go to previous/next visited revision (in navigation history) **Backspace** set current revision the current start revision (hide any revision above it) **Shift+Backspace** clear the start revision value """ def get_options_helpmsg(rest=False): """display hgview full list of configuration options """ from config import get_option_descriptions options = get_option_descriptions(rest) msg = """ Configuration options ===================== The following options can be set in the ``[hgview]`` section of a Mercurial configuration file. :Note: *User interface specific configuration* is possible. You can add a ``qt.`` or ``raw.`` prefix to each option in order to target a particular user interface. Without any prefix, the value is used as default for both user interfaces. """ msg += '\n'.join(["- " + v for v in options]) + '\n' msg += """ The value of 'users' should be the name of a file describing well-known users, like:: --8<------------------------------------------- # file ~/.hgusers id=david alias=david.douard@logilab.fr alias=david@logilab.fr alias=David Douard color=#FF0000 id=ludal alias=ludovic.aubry@logilab.fr alias=ludal@logilab.fr alias=Ludovic Aubry color=#00FF00 --8<------------------------------------------- This allows authors to be shown with consistent coloring in the graphlog browser, even if they use different usernames. """ return msg long_help_msg = help_msg + get_options_helpmsg() hgview-1.9.0/hgviewlib/curses/0000775000015700001640000000000012607506603017073 5ustar narvalnarval00000000000000hgview-1.9.0/hgviewlib/curses/helpviewer.py0000644000015700001640000000663312607505500021620 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ Module that contains the help body. """ import urwid from urwid import AttrWrap, Text, Padding, ListBox, SimpleListWalker, Divider from hgviewlib.curses import Body, utils, hg_command_map class HelpViewer(Body): """A body to display a help message (or the global program help)""" def __init__(self, messages=None, *args, **kwargs): # cut line ? if messages is not None: contents = [Text(messages)] else: contents = [] #keybindings contents.extend(section('Keybindings')) messages = [] keys = hg_command_map.keys() longest = max(len(key) for key in keys) for name, cmd in hg_command_map.items(): messages.append(('ERROR', name.rjust(longest))) messages.append(('WARNING', ' | ')) messages.append(cmd) messages.append('\n') contents.append(Text(messages)) # mouse contents.extend(section('Mouse')) messages = [('ERROR', 'button 1'), ('WARNING', ' | '), 'Show context\n', ('ERROR', 'button 3'), ('WARNING', ' | '), 'Hide context\n', ('ERROR', 'button 4'), ('WARNING', ' | '), 'Scroll up\n', ('ERROR', 'button 5'), ('WARNING', ' | '), 'Scroll down\n', ] contents.append(Text(messages)) # commands contents.extend(section('Commands List')) messages = [] for name, helpmsg in utils.help_commands(): messages.append(('ERROR', '\ncommand: "%s"\n'%name)) messages.extend(helpmsg) contents.append(Text(messages)) listbox = ListBox(SimpleListWalker(contents)) super(HelpViewer, self).__init__(body=listbox, *args, **kwargs) def mouse_event(self, size, event, button, *args, **kwargs): """Scroll content""" if urwid.util.is_mouse_press(event): if button == 4: self.keypress(size, 'page up') return elif button == 5: self.keypress(size, 'page down') return return super(HelpViewer, self).mouse_event(size, event, button, *args, **kwargs) def section(title): """Return a list of widgets that may used as separators""" contents = [] contents.append(Divider('-')) contents.append(AttrWrap(Padding(Text(title), 'center'), 'CRITICAL')) contents.append(Divider('-')) return contents hgview-1.9.0/hgviewlib/curses/manifest.py0000644000015700001640000001263012607505500021246 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ Module that contains the help body. """ from urwid import AttrWrap, ListWalker from urwid.signals import emit_signal from hgviewlib.util import isbfile, bfilepath, exec_flag_changed from hgviewlib.curses import SelectableText DIFF = 'diff' FILE = 'file' class ManifestWalker(ListWalker): """ Walk through modified files. """ signals = ['focus changed'] def __init__(self, walker, ctx, manage_description=False, *args, **kwargs): """ :ctx: mercurial context instance :manage_description: display context description as a file if True """ self._cached_flags = {} self._walker = walker self._ctx = ctx self.manage_description = manage_description if manage_description: self._focus = -1 else: self._focus = 0 if self._ctx: self._files = tuple(self._ctx.files()) else: self._files = () super(ManifestWalker, self).__init__(*args, **kwargs) def get_filename(self): """Return focused file name""" if self._focus < 0: return return self._files[self._focus] def set_filename(self, filename): """change focus element by giving the corresponding file name""" try: focus = self._files.index(filename) except ValueError: # focus on description focus = -1 self.set_focus(focus) filename = property(get_filename, set_filename, None, 'File name under focus.') def __len__(self): return len(self._files) def get_ctx(self): """Return the current context""" return self._ctx def set_ctx(self, ctx, reset_focus=True): """set the current context (obsolete the content)""" self._cached_flags.clear() self._ctx = ctx self._files = tuple(self._ctx.files()) if reset_focus: del self.focus self._modified() ctx = property(get_ctx, set_ctx, None, 'Current changeset context') def get_focus(self): """return (focused widget, position)""" try: return self.data(self._focus), self._focus except IndexError: return None, None def set_focus(self, focus): """set the focused widget giving the position.""" self._focus = focus emit_signal(self, 'focus changed', self.filename) def reset_focus(self): """Reset focus""" if self.manage_description: self._focus = -1 else: self._focus = 0 emit_signal(self, 'focus changed', self.filename) focus = property(lambda self: self._focus, set_focus, reset_focus, 'focus index') def get_prev(self, pos): """return (widget, position) before position `pos` or (None, None)""" focus = pos - 1 try: return self.data(focus), focus except IndexError: return None, None def get_next(self, pos): """return (widget, position) after position `pos` or (None, None)""" focus = pos + 1 try: return self.data(focus), focus except IndexError: return None, None def data(self, focus): """return widget a position `focus`""" if self._ctx is None: raise IndexError('context is None') if (focus < -1) or ((not self.manage_description) and (focus < 0)): raise IndexError(focus) if focus == -1: return AttrWrap(SelectableText('-*- description -*-', align='right', wrap='clip'), 'DEBUG', 'focus') filename = self._files[focus] # Computing the modification flag may take a long time, so cache it. flag = self._cached_flags.get(filename) if flag is None: flag = self._cached_flags.setdefault(filename, self._walker.graph.fileflag(filename, self._ctx.rev())) if not isinstance(flag, str): # I don't know why it could occur :P flag = '?' return AttrWrap(SelectableText(filename, align='right', wrap='clip'), flag, 'focus') def filedata(self, filename): '''return (modification flag, file content)''' if isbfile(filename): filename = bfilepath(filename) graph = self._walker.graph return graph.filedata(filename, self._ctx.rev(), 'diff', flag=self._cached_flags.get(filename)) def clear(self): """clear content""" self._cached_flags.clear() self._files = () del self.focus self._modified() hgview-1.9.0/hgviewlib/curses/utils.py0000644000015700001640000003077012607505500020605 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ A Module that contains usefull utilities. """ import shlex import fnmatch from urwid.command_map import CommandMap from hgviewlib.curses.exceptions import UnknownCommand, RegisterCommandError __all__ = ['register_command', 'unregister_command', 'connect_command', 'disconnect_command', 'emit_command', 'help_command', 'complete_command', 'CommandArg', 'hg_command_map', 'History', ] # ____________________________________________________________________ commands class CommandArg(object): def __init__(self, name, parser, help): self.name = name self.parser = parser self.help = help class Commands(object): """A class that handle commands using a signal-like system. * You shall *register* a command before using it. * Then you may want to *connect* callbacks to commands and call. * They will be processed while *emitting* the cammands. * Note that you can pick up *help* about commands. You can fix callback arguments when connecting and/or emmitting a command. Emit method accept a command line string. This command line can only be a command name if all arguments for all callbacks have been fixed (or if they are optionals). Otherwise the command options can be automatically parsed by giving `CommandArg`s to register. """ def __init__(self): self._args = {} self._helps = {} self._calls = {} def register(self, names, help, *args): """Register a command to make it available for connecting/emitting. :names: the command name or a list of aliases. :args: `CommandArg` instances. >>> from hgviewlib.curses import utils >>> import urwid >>> args = (utils.CommandArg('arg1', int, 'argument1'), ... utils.CommandArg('arg2', float, 'argument2'),) >>> utils.register_command('foo', 'A command', *args) >>> out = utils.unregister_command('foo') """ if isinstance(names, str): names = [names] for name in names: if name in self._helps: raise RegisterCommandError( 'Command "%s" already registered' % name) for arg in args: if not isinstance(arg, CommandArg): raise RegisterCommandError( 'Command arguments description type must be a CommandArg') calls = [] # all points to the same values for name in names: self._args[name] = args self._helps[name] = help self._calls[name] = calls def __contains__(self, name): """Do not use""" return name in self._helps def unregister(self, name): """Unregister a command.""" if name not in self: return help = self._helps.pop(name) args = self._args.pop(name) calls = self._calls.pop(name) return help, args, calls def connect(self, name, callback, args=None, kwargs=None): """Disconnect the ``callback`` associated to the given ``args`` and ``kwargs`` from the command ``name``. See documentation of ``emit_command`` for details about ``args`` and ``kwarg``. """ if name not in self: raise RegisterCommandError( 'You must register the command "%s" before connecting a callback.' % name) if args is None: args = () if kwargs is None: kwargs = {} self._calls[name].append((callback, args, kwargs)) def disconnect(self, name, callback, args=None, kwargs=None): """Disconnect the ``callback`` associated to the given ``args`` and ``kwargs`` from the command ``name``. >>> from hgviewlib.curses import utils >>> utils.register_command('foo', 'A command') >>> func = lambda *a, **k: True >>> utils.connect_command('foo', func, (1,2), {'a':0}) >>> utils.disconnect_command('foo', func, (1,2), {'a':0}) >>> out = utils.unregister_command('foo') """ if args is None: args = () if kwargs is None: kwargs = {} try: self._calls[name].remove((callback, args, kwargs)) except KeyError: raise RegisterCommandError('Command not registered: %s' % name) except ValueError: raise RegisterCommandError('Callbacks not connected.') def emit(self, cmdline, args=None, kwargs=None): """Call all callbacks connected to the command previously registered. Callbacks are processed as following:: registered_callback(*args, **kwargs) where ``args = connect_args + emit_args + commandline_args`` and ``kwargs = connect_kwargs.copy(); kwargs.update(emit_kwargs)`` :cmdline: a string that contains the complete command line. :return: True is a callback return True, else False """ result = False name, rawargs = (cmdline.strip().split(None, 1) + [''])[:2] if not name in self: raise UnknownCommand(name) cmdargs = [] if rawargs and self._args[name]: data = self._args[name] # shlex does not support unicode, so we have to encode/decode the # command line arguments = (item.decode('utf-8') for item in shlex.split(rawargs.encode('utf-8'))) for idx, arg in enumerate(arguments): try: parser = data[idx].parser except IndexError: parser = str cmdargs.append(parser(arg)) cmdargs = tuple(cmdargs) result = False for _func_, _args_, _kwargs_ in self._calls[name]: ags = _args_ + (args or ()) + cmdargs kws = _kwargs_.copy() kws.update(kwargs or {}) result |= bool(_func_(*ags, **kws)) return result def help(self, name): """Return help for command ``name`` suitable for urwid.Text. >>> from hgviewlib.curses import utils >>> import urwid >>> args = (utils.CommandArg('arg1', int, 'argument1'), ... utils.CommandArg('arg2', float, 'argument2'),) >>> utils.register_command('foo', 'A command', *args) >>> data = urwid.Text(utils.help_command('foo')).render((20,)).text >>> print '|%s|' % '|\\n|'.join(data) |usage: foo arg1 arg2| |A command | |:arg1: argument1 | |:arg2: argument2 | | | >>> out = utils.unregister_command('foo') """ if name not in self._helps: raise RegisterCommandError( 'Unknown command "%s"' % name) help = self._helps[name] args = self._args[name] message = [('default', 'usage: '), ('WARNING', name)] \ + [('DEBUG', ' ' + a.name) for a in args] \ + [('default', '\n%s\n' % help)] for arg in args: message.append(('default', ':')) message.append(('DEBUG', arg.name)) message.append(('default', ': ')) message.append(arg.help + '\n') return message def helps(self): """Return a generator that gives (name, help) for each command""" for name in sorted(self._helps.keys()): yield name, self.help(name) def complete(self, line): """ Return command name candidates that complete ``line``. It uses fnmatch to match candidates, so ``line`` may contains wildcards. """ if not line: return self._helps.keys() line = tuple(line.split(None, 1)) out = line if len(line) == 1: cmd = line[0] + '*' return tuple(sorted(fnmatch.filter(self._args, cmd))) # Instanciate a Commands object to handle command from a global scope. #pylint: disable=C0103 _commands = Commands() register_command = _commands.register unregister_command = _commands.unregister connect_command = _commands.connect disconnect_command = _commands.disconnect emit_command = _commands.emit help_command = _commands.help help_commands = _commands.helps complete_command = _commands.complete #pylint: enable=C0103 class History(list): def __init__(self, list=None, current=None): super(History, self).__init__(list or ()) self.insert(0, current) self.position = 0 def get(self, position, default=None): """ Return the history entry at `position` or `default` if not found. """ try: return self[position] except IndexError: return default def get_next(self, default=None): """Return the next entry of the history""" self.position += 1 self.position %= len(self) return self.get(self.position, default) def get_prev(self, default=None): """Return the previous entry of the history""" self.position -= 1 self.position %= len(self) return self.get(self.position, default) def reset_position(self): """reset the position of the history pointer""" self.position = 0 def set_current(self, current): self[0] = current # _________________________________________________________________ command map class HgCommandMap(object): """Map keys to more explicit action names.""" _command_defaults = ( ('f1', '@help'), ('enter', 'validate'), ('m', '@maximize-context'), ('.', '@toggle-hidden'), # Qt interface ('f5', 'command key'), ('esc', 'escape'), ('ctrl l', '@refresh'), ('ctrl w', 'close pane'), ('up', 'graphlog up'), ('down', 'graphlog down'), ('left', 'manifest up'), ('right', 'manifest down'), ('meta up', 'source up'), ('meta down', 'source down'), ('page up', 'graphlog page up'), ('page down', 'graphlog page down'), ('home', 'manifest page up'), ('end', 'manifest page down'), ('insert', 'source page up'), ('delete', 'source page down'), # vim interface (':', 'command key'), #'esc','escape', already set in Qt interface ('r', '@refresh'), ('q', 'close pane'), ('k', 'graphlog up'), ('j', 'graphlog down'), ('h', 'manifest up'), ('l', 'manifest down'), ('p', 'source up'), ('n', 'source down'), ('K', 'graphlog page up'), ('J', 'graphlog page down'), ('H', 'manifest page up'), ('L', 'manifest page down'), ('P', 'source page up'), ('N', 'source page down'), # emacs interface ('meta x', 'command key'), ('ctrl g', 'escape'), ('ctrl v', '@refresh'), ('ctrl k', 'close pane'), ('ctrl p', 'graphlog up'), ('ctrl n', 'graphlog down'), ('ctrl b', 'manifest up'), ('ctrl f', 'manifest down'), ('ctrl a', 'source up'), ('ctrl e', 'source down'), ('meta p', 'graphlog page up'), ('meta n', 'graphlog page down'), ('meta b', 'manifest page up'), ('meta f', 'manifest page down'), ('meta a', 'source page up'), ('meta e', 'source page down'), ) def __init__(self): self._map = dict(self._command_defaults) def __getitem__(self, key): """a.__getitem__(key) <=> a[key] return an explicit name associated to the key or None if not found. """ return self._map.get(key) def items(self): """return the list of (registered keys, associated name)""" return self._command_defaults def keys(self): """return the list of registered keys""" return self._map.keys() hg_command_map = HgCommandMap() hgview-1.9.0/hgviewlib/curses/exceptions.py0000644000015700001640000000237112607505500021622 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ Exceptions classes used by hgview curses """ class HgviewCursesException(Exception): """Base class for all hgview curses exception """ class CommandError(ValueError, HgviewCursesException): """Error that occurs while calling a command""" class UnknownCommand(StopIteration, HgviewCursesException): """Error that occurs when callback not found""" class RegisterCommandError(KeyError, HgviewCursesException): """Error that occurs when a conflict occurs while registering a command""" hgview-1.9.0/hgviewlib/curses/graphlog.py0000644000015700001640000003320112607505500021240 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . ''' Contains a listbox definition that walk the repo log and display an ascii graph ''' try: from itertools import izip_longest as zzip zzip(()) # force check over lazy import except (ImportError, TypeError): # python2.5 support from itertools import repeat, chain class ZipExhausted(Exception): pass def zzip(*args, **kwds): # izip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D- fillvalue = kwds.get('fillvalue') counter = [len(args) - 1] def sentinel(): if not counter[0]: raise ZipExhausted counter[0] -= 1 yield fillvalue fillers = repeat(fillvalue) iterators = [chain(it, sentinel(), fillers) for it in args] try: while iterators: yield tuple(iterator.next() for iterator in iterators) except ZipExhausted: pass from logging import warn from mercurial.node import short from urwid import AttrMap, Text, ListWalker, Columns, WidgetWrap, emit_signal from hgviewlib.hgpatches.graphmod import (_fixlongrightedges, _getnodelineedgestail, _drawedges, _getpaddingline) from hgviewlib.util import tounicode from hgviewlib.hggraph import getlog, gettags, getdate, HgRepoListWalker from hgviewlib.curses import connect_command, SelectableText # __________________________________________________________________ constants COLORS = ["brown", "dark red", "dark magenta", "dark blue", "dark cyan", "dark green", "yellow", "light red", "light magenta", "light blue", "light cyan", "light green"] _COLUMNMAP = { 'ID': lambda m, c, g: c.rev() is not None and str(c.rev()) or "", 'Log': getlog, 'Author': lambda m, c, g: tounicode((c.user() if c.node() else '').split('<',1)[0]), 'Date': getdate, 'Tags': gettags, 'Bookmarks': lambda m, c, g: ', '.join(c.bookmarks() or ()), 'Branch': lambda m, c, g: c.branch() != 'default' and c.branch(), 'Filename': lambda m, c, g: g.extra[0], 'Phase': lambda model, ctx, gnode: ctx.phasestr(), } GRAPH_MIN_WIDTH = 6 # ____________________________________________________________________ classes class RevisionsWalker(ListWalker): """ListWalker-compatible class for browsing log changeset. """ signals = ['focus changed'] _columns = HgRepoListWalker._columns _allfields = (('Bookmarks', 'Branch', 'Tags', 'Log'),) _allcolumns = (('Date', 16), ('Author', 20), ('ID', 6),) def __init__(self, walker, branch='', fromhead=None, follow=False, *args, **kwargs): self._data_cache = {} self._focus = 0 self.walker = walker super(RevisionsWalker, self).__init__(*args, **kwargs) self.asciistate = [0, 0] # graphlog.asciistate() def connect_commands(self): """Connect usefull commands to callbacks""" connect_command('goto', self.set_rev) def _modified(self): """obsolete widget content""" super(RevisionsWalker, self)._modified() def _invalidate(self): """obsolete rendering cache""" self._data_cache.clear() super(RevisionsWalker, self)._modified() @staticmethod def get_color(idx, ignore=()): """ Return a color at index 'n' rotating in the available colors. 'ignore' is a list of colors not to be chosen. """ colors = [x for x in COLORS if x not in ignore] if not colors: # ghh, no more available colors... colors = COLORS return colors[idx % len(colors)] def data(self, pos): """Return a widget and the position passed.""" # cache may be very huge on very big repo # (cpython for instance: >1.5GB) if pos in self._data_cache: # speed up rendering return self._data_cache[pos], pos widget = self.get_widget(pos) if widget is None: return None, None self._data_cache[pos] = widget return widget, pos def get_widget(self, pos): """Return a widget for the node""" if pos in self._data_cache: # speed up rendering return self._data_cache[pos], pos try: self.walker.ensureBuilt(row=pos) except ValueError: return None gnode = self.walker.graph[pos] ctx = self.walker.repo.changectx(gnode.rev) # prepare the last columns content txts = [] for graph, fields in zzip(self.graphlog(gnode, ctx), self._allfields): graph = graph or '' fields = fields or () txts.append(graph) txts.append(' ') for field in fields: if field not in self._columns: continue txt = _COLUMNMAP[field](self.walker, ctx, gnode) if not txt: continue txts.append((field, txt)) txts.append(('default', ' ')) txts.pop() # remove pending space txts.append('\n') txts.pop() # remove pending newline # prepare other columns txter = lambda col, sz: Text( (col, _COLUMNMAP[col](self.walker, ctx, gnode)[:sz]), align='right', wrap='clip') columns = [('fixed', sz, txter(col, sz)) for col, sz in self._allcolumns if col in self._columns] columns.append(SelectableText(txts, wrap='clip')) # tune style spec_style = {} # style modifier for normal foc_style = {} # style modifier for focused all_styles = set(self._columns) | set(['GraphLog', 'GraphLog.node', None]) important_styles = set(['ID', 'GraphLog.node']) if ctx.obsolete(): spec_style.update(dict.fromkeys(all_styles, 'obsolete')) # normal style: use special styles for working directory and tip style = None if gnode.rev is None: style = 'modified' # pending changes elif gnode.rev in self.walker.wd_revs: style = 'current' if style is not None: spec_style.update(dict.fromkeys(important_styles, style)) # focused style: use special styles for working directory and tip foc_style.update(dict.fromkeys(all_styles, style or 'focus')) foc_style.update(dict.fromkeys(important_styles, 'focus.alternate')) # wrap widget with style modified widget = AttrMap(Columns(columns, 1), spec_style, foc_style) return widget def graphlog(self, gnode, ctx): """Return a generator that get lines of graph log for the node """ # define node symbol if gnode.rev is None: char = '!' # pending changes elif not getattr(ctx, 'applied', True): char = ' ' elif set(ctx.tags()).intersection(self.walker.mqueues): char = '*' else: phase = ctx.phase() try: char = 'o#^'[phase] except IndexError: warn('"%(node)s" has an unknown phase: %(phase)i', {'node':short(ctx.node()), 'phase':phase}) char = '?' # build the column data for the graphlogger from data given by hgview curcol = gnode.x curedges = [(start, end) for start, end, color, fill in gnode.bottomlines if start == curcol] try: prv, nxt, _, _ = zip(*gnode.bottomlines) prv, nxt = len(set(prv)), len(set(nxt)) except ValueError: # last prv, nxt = 1, 0 coldata = (curcol, curedges, prv, nxt - prv) self.asciistate = self.asciistate or [0, 0] return hgview_ascii(self.asciistate, char, len(self._allfields), coldata) def get_focus(self): """Get focused widget""" try: return self.data(self._focus) except IndexError: if self._focus > 0: self._focus = 0 else: self._focus = 0 try: return self.data(self._focus) except: return None, None def set_focus(self, focus=None): """change focused widget""" self._focus = focus or 0 emit_signal(self, 'focus changed', self.get_ctx()) focus = property(lambda self: self._focus, set_focus, None, 'focused widget index') def get_rev(self): """Return revision of the focused changeset""" if self._focus >= 0: return self.walker.graph[self._focus].rev def set_rev(self, rev=None): """change focused widget to the given revision ``rev``.""" if rev is None: self.set_focus(0) else: self.set_focus(self.walker.graph.index(rev or 0)) self._invalidate() rev = property(get_rev, set_rev, None, 'current revision') def get_ctx(self): """return context of the focused changeset""" if self.focus >= 0: return self.walker.repo.changectx(self.rev) def get_next(self, start_from): """get the next widget to display""" focus = start_from + 1 try: return self.data(focus) except IndexError: return None, None def get_prev(self, start_from): """get the previous widget to display""" focus = start_from - 1 if focus < 0: return None, None try: return self.data(focus) except IndexError: return None, None # __________________________________________________________________ functions def hgview_ascii(state, char, height, coldata): """prints an ASCII graph of the DAG takes the following arguments (one call per node in the graph): :param state: Somewhere to keep the needed state in (init to [0, 0]) :param char: character to use as node's symbol. :param height: minimal line number to use for this node :param coldata: (idx, edges, ncols, coldiff) * idx: column index for the current changeset * edges: a list of (col, next_col) indicating the edges between the current node and its parents. * ncols: number of columns (ongoing edges) in the current revision. * coldiff: the difference between the number of columns (ongoing edges) in the next revision and the number of columns (ongoing edges) in the current revision. That is: -1 means one column removed; 0 means no columns added or removed; 1 means one column added. :note: it is a Modified version of Joel Rosdahl graphlog extension for mercurial """ idx, edges, ncols, coldiff = coldata # graphlog is broken with multiple parent. But we have ignore that to allow # some support of obsolete relation display # assert -2 < coldiff < 2 assert height > 0 if coldiff == -1: _fixlongrightedges(edges) # add_padding_line says whether to rewrite add_padding_line = (height > 2 and coldiff == -1 and [x for (x, y) in edges if x + 1 < y]) # fix_nodeline_tail says whether to rewrite fix_nodeline_tail = height <= 2 and not add_padding_line # nodeline is the line containing the node character (typically o) nodeline = ["|", " "] * idx nodeline.extend([('GraphLog.node', char), " "]) nodeline.extend(_getnodelineedgestail(idx, state[1], ncols, coldiff, state[0], fix_nodeline_tail)) # shift_interline is the line containing the non-vertical # edges between this entry and the next shift_interline = ["|", " "] * idx if coldiff == -1: n_spaces = 1 edge_ch = "/" elif coldiff == 0: n_spaces = 2 edge_ch = "|" else: n_spaces = 3 edge_ch = "\\" shift_interline.extend(n_spaces * [" "]) shift_interline.extend([edge_ch, " "] * (ncols - idx - 1)) # draw edges from the current node to its parents _drawedges(edges, nodeline, shift_interline) # lines is the list of all graph lines to print lines = [nodeline] if add_padding_line: lines.append(_getpaddingline(idx, ncols, edges)) if not set(shift_interline).issubset(set([' ', '|'])): # compact lines.append(shift_interline) # make sure that there are as many graph lines as there are # log strings if len(lines) < height: extra_interline = ["|", " "] * (ncols + coldiff) while len(lines) < height: lines.append(extra_interline) # print lines indentation_level = max(ncols, ncols + coldiff) for line in lines: # justify to GRAPH_MIN_WIDTH for convenience if len(line) < GRAPH_MIN_WIDTH: line.append(' ' * (GRAPH_MIN_WIDTH - len(line))) yield [('GraphLog', item) if isinstance(item, basestring) else item for item in line] # ... and start over state[0] = coldiff state[1] = idx hgview-1.9.0/hgviewlib/curses/application.py0000644000015700001640000003001112607505500021734 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ Application utilities. """ import threading import logging import sys from urwid import AttrWrap, MainLoop, VERSION as URWID_VERSION from hgviewlib.application import HgViewApplication, ApplicationError from hgviewlib.curses.hgrepoviewer import RepoViewer from hgviewlib.curses import MainFrame, emit_command, activate_delayed_signals try: import pygments from pygments.token import Token, _TokenType except ImportError: # pylint: disable=C0103 pygments = None # pylint: enable=C0103 # _________________________________________________________________ Application class HgViewUrwidApplication(HgViewApplication): """ HgView application using urwid. """ HgRepoViewer = RepoViewer def __init__(self, *args, **kwargs): super(HgViewUrwidApplication, self).__init__(*args, **kwargs) self.viewer = AttrWrap(self.viewer, 'body') mainframe = MainFrame('repoviewer', self.viewer) screen = self.get_screen() self.mainloop = MainLoop(mainframe, PALETTE, screen) connect_logging(self.mainloop, level=logging.DEBUG) mainframe.register_commands() self.enable_inotify() activate_delayed_signals(self.mainloop) self.mainframe = mainframe # register_command('alarm', 'process callback in a given seconds', def get_screen(self): """return the screen instance to use""" if self.opts.interface == 'curses' and URWID_VERSION < (1, 0, 0): raise ApplicationError('The "curses" interface can not be used ' 'with old installed urwid version ' '%s < 1.0.0. Use the "raw" interface' % (URWID_VERSION, )) if self.opts.interface == 'raw': from urwid.raw_display import Screen elif self.opts.interface == 'curses': from urwid.curses_display import Screen if pygments: return patch_screen(Screen)() return Screen() def enable_inotify(self): """enable inotify watching""" enable_inotify = True # XXX config optimize_inotify = True # XXX config if enable_inotify: if optimize_inotify: import ctypes.util orig = ctypes.util.find_library ctypes.util.find_library = lambda lib: None # dirty optimization inotify(self.mainloop) if optimize_inotify: ctypes.util.find_library = orig def exec_(self): '''main entry point''' if '--profile' in sys.argv or '--time' in sys.argv: self.mainloop._run = self.mainloop.draw_screen out = self.mainloop.run() self.mainframe.unregister_commands() return out # _____________________________________________________________________ inotify def inotify(mainloop): """add inotify watcher to the mainloop""" try: from hgviewlib.inotify import Inotify as Inotify except ImportError: return class UrwidInotify(Inotify): """Inotify handler that can be connected to as urwid mainloop.""" def __init__(self, *args, **kwargs): super(UrwidInotify, self).__init__(*args, **kwargs) self._input_timeout = None def process_finally(self): """Really process the inotify event""" self._input_timeout = None super(UrwidInotify, self).process() def process_on_any_event(self): """Process all inotify events and prevent over-processing""" # get all events on every files. # Ignore files that shall be ignored be mercurial # Also ignore hg-checkexec* files that are created by mercurial # to check available file status. for fname in self.read_events(): if fname.startswith(('hg-checkexec', 'hg-checklink')): break if self.repo.dirstate._dirignore(fname): break else: # use the urwid mainloop to schedule the screen refreshing in 0.2s # and ignore events received during this time. # It prevents over-refreshing (See ../inotify.py comments). if self._input_timeout is None: self._input_timeout = mainloop.set_alarm_in( 0.2, lambda *args: self.process_finally()) try: refresh = lambda: emit_command('refresh') inot = UrwidInotify(mainloop.widget.get_body().repo, refresh) except: return mainloop.event_loop.watch_file(inot.get_fd(), inot.process_on_any_event) # add watchers thought a thread to reduce start duration with a big repo threading.Thread(target=inot.update).start() # ________________________________________________________________ patch screen def patch_screen(screen_cls): """ Return a patched screen class that allows parent token inheritance in the palette """ class Palette(dict): """Special dictionary that take into account parent token inheritance. """ def __contains__(self, key): if super(Palette, self).__contains__(key): return True if (not isinstance(key, _TokenType)) or (key.parent is None): return False if key.parent in self: # function is now recursive self[key] = self[key.parent] # cache + __getitem__ ok return True return False has_key = __contains__ class PatchedScreen(screen_cls, object): """hack Screen to allow parent token inheritance in the palette""" # Use a special container for storing style definition. This container # take into account parent token inheritance # raw_display.Screen store the palette definition in the container # ``_pal_escape``, web_display and curses display in ``palette`` and # ``attrconv`` def __init__(self, *args): self._hgview_palette = None self._hgview_attrconv = None # mro problem with web_display, so do not use super screen_cls.__init__(self) def _hgview_get_palette(self): """Return the palette""" return self._hgview_palette def _hgview_set_palette(self, value): """Set the palette""" self._hgview_palette = Palette() if value: self._hgview_palette.update(value) # pylint: disable=E0602 _pal_escape = property(_hgview_get_palette, _hgview_set_palette) palette = _pal_escape def _hgview_get_attrconv(self): """Return the palette""" return self._hgview_attrconv def _hgview_set_attrconv(self, value): """Set the palette""" self._hgview_attrconv = Palette() if value: self._hgview_attrconv.update(value) # pylint: disable=E0602 attrconv = property(_hgview_get_attrconv, _hgview_set_attrconv) return PatchedScreen # _____________________________________________________________________ logging def connect_logging(mainloop, level=logging.INFO): '''Connect logging to the hgview console application. (The widget of the mainloop must be a ``MainFrame`` instance) You may add 'DEBUG', 'WARNING', 'ERROR' and 'CRITICAL' styles in the palette. ''' class ConsoleHandler(logging.Handler): '''Handler for logging to the footer of a ``MainFrame`` instance. You shall prefer to link logging and you application by using the ``connect_logging(...)`` function. ''' def __init__(self, callback, redraw, redraw_levelno=logging.CRITICAL): """ :param callback: A function called to display a message as ``callback(style, levelname, message)`` where: * ``levelname`` is the name of the message level * ``message`` is the message to display Mostly, it is the ``set`` method of a ``Footer`` instance. :param redraw: a function that performs the screen redrawing """ self.callback = callback self.redraw = redraw self.redraw_levelno = redraw_levelno logging.Handler.__init__(self) def emit(self, record): """emit a record""" if isinstance(record.msg, list): # urwid style name = 'default' msg = record.msg else: name = record.levelname msg = self.format(record) self.callback(name, msg) if record.levelno >= self.redraw_levelno: self.flush() def flush(self): try: self.redraw() except AssertionError: pass logger = logging.getLogger() logger.setLevel(level) display = lambda style, msg: mainloop.widget.footer.set(style, msg, '') handler = ConsoleHandler(display, mainloop.draw_screen) logger.addHandler(handler) # ________________________________________________________________ patch screen PALETTE = [ ('default','default','default'), ('body','default','default', 'standout'), ('banner','black','light gray', 'bold'), ('focus','black','dark cyan', 'bold'), ('focus.alternate','black','dark magenta', 'bold'), ('current', 'black', 'dark green', 'bold'), ('modified', 'black', 'brown', 'bold'), # logging ('DEBUG', 'dark magenta', 'default'), ('INFO', 'dark gray', 'default'), ('WARNING', 'brown', 'default'), ('ERROR', 'dark red', 'default'), ('CRITICAL', 'light red', 'default'), # graphlog ('ID', 'brown', 'default', 'standout'), ('Log', 'default', 'default'), ('GraphLog', 'default', 'default', 'bold'), ('GraphLog.node', 'default', 'default', 'bold'), ('Author', 'dark blue', 'default', 'bold'), ('Date', 'dark green', 'default', 'bold'), ('Tags', 'yellow', 'dark red', 'bold'), ('Bookmarks', 'default', 'dark blue'), ('Branch', 'yellow', 'default', 'bold'), ('Filename', 'white', 'default', 'bold'), ('obsolete', 'dark cyan', 'default'), # filelist ('+', 'dark green', 'default'), ('-', 'dark red', 'default'), ('=', 'default', 'default'), ('?', 'brown', 'default'), ] if pygments: PALETTE += [ (Token, 'default', 'default'), (Token.Text, 'default', 'default'), (Token.Comment, 'dark gray', 'default'), (Token.Punctuation, 'white', 'default', 'bold'), (Token.Operator, 'light blue', 'default'), (Token.Literal, 'dark magenta', 'default'), (Token.Name, 'default', 'default'), (Token.Name.Builtin, 'dark blue', 'default'), (Token.Name.Namespace, 'dark blue', 'default'), (Token.Name.Builtin.Pseudo, 'dark blue', 'default'), (Token.Name.Exception, 'dark blue', 'default'), (Token.Name.Decorator, 'dark blue', 'default'), (Token.Name.Class, 'dark blue', 'default'), (Token.Name.Function, 'dark blue', 'default'), (Token.Keyword, 'light green', 'default'), (Token.Generic.Deleted, 'dark red', 'default'), (Token.Generic.Inserted, 'dark green', 'default'), (Token.Generic.Subheading, 'dark magenta', 'default', 'bold'), (Token.Generic.Heading, 'black', 'dark magenta'), ] hgview-1.9.0/hgviewlib/curses/__init__.py0000644000015700001640000000541012607505500021175 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ console interface for hgview. """ # disable lazy import for urwid from mercurial import demandimport demandimport.ignore.extend([ 'urwid.html_fragment', 'urwid.tests', 'urwid', 'urwid.escape', 'urwid.command_map', 'urwid.signals', 'urwid.version', 'urwid.util', 'urwid.display_common', 'urwid.font', 'urwid.old_str_util', 'urwid.lcd_display', 'urwid.raw_display', 'urwid.split_repr', 'urwid.listbox', 'urwid.decoration', 'urwid.widget', 'urwid.graphics', 'urwid.wimp', 'urwid.container', 'urwid.web_display', 'urwid.curses_display', 'urwid.text_layout', 'urwid.compat', 'urwid.main_loop', 'urwid.monitored_list', 'urwid.__init__', 'urwid.vterm_test', 'urwid.treetools', 'urwid.canvas', 'urwid.vterm']) # use __all__ in the corresponding modules # pylint: disable=W0401 from hgviewlib.curses.utils import * from hgviewlib.curses.exceptions import * from hgviewlib.curses.widgets import * from hgviewlib.curses.mainframe import MainFrame # pylint: enable=W0401 # patching urwid # patch urwid signals system in order to allow delayed signals import urwid.signals urwid.signals.delay_emit_signal = lambda o, n, d, *a: urwid.signals.emit_signal(o, n, *a) def activate_delayed_signals(mainloop): """ patch urwid signals system in order to allow delayed signals """ import urwid.signals emit = urwid.signals.emit_signal if mainloop is None: urwid.signals.delay_emit_signal = lambda o, n, d, *a: emit(o, n, *a) return memorizer = {} def delay_emit_signal(obj, name, delay, *args): """Same as emit_signal but really process the signal in `delay` seconds""" emit_hash = (id(obj), name) # remove previous alarm even if already processed if emit_hash in memorizer: mainloop.remove_alarm(memorizer[emit_hash]) delayed_emit = lambda *ignored: emit(obj, name, *args) handle = mainloop.set_alarm_in(delay, delayed_emit) memorizer[(id(obj), name)] = handle urwid.signals.delay_emit_signal = delay_emit_signal hgview-1.9.0/hgviewlib/curses/widgets.py0000644000015700001640000001344712607505500021115 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ A module that contains usefull widgets. """ from urwid import Frame, Text, AttrWrap, ListBox, signals from urwid.util import is_mouse_press try: import pygments from pygments import lex, lexers from pygments.util import ClassNotFound except ImportError: # pylint: disable=C0103 pygments = None from hgviewlib.curses.canvas import apply_text_layout __all__ = ['Body', 'ScrollableListBox', 'SelectableText', 'SourceText'] class SelectableText(Text): """A selectable Text widget""" _selectable = True keypress = lambda self, size, key: key class Body(Frame): """A suitable widget that shall be used as a body for the mainframe. +------------------+ | | | Body | | | +------------------+ | Text with title | +------------------+ Use the ``title`` property to change the footer text. """ def __init__(self, body): footer = AttrWrap(Text(''), 'banner') super(Body, self).__init__(body=body, footer=footer, header=None, focus_part='body') def _get_title(self): """return the title""" return self._footer.get_text() def _set_title(self, title): """set the title text""" self._footer.set_text(title) def _clear_title(self): """clear the title text""" self._footer.set_title('') title = property(lambda self: self.footer.text, _set_title, _clear_title, 'Body title') def register_commands(self): """register commands""" pass def unregister_commands(self): """unregister commands""" pass class ScrollableListBox(ListBox): """Scrollable Content ListBox using mouse buttons 4/5""" # pylint: disable=R0913 def mouse_event(self, size, event, button, col, row, focus): """Scroll content""" if is_mouse_press(event): if button == 4: self.keypress(size, 'page up') return elif button == 5: self.keypress(size, 'page down') return return super(ScrollableListBox, self).mouse_event(size, event, button, col, row, focus) # pylint: enable=R0913 class SourceText(SelectableText): """A widget that display source code content. It can number lines and highlight content using pygments. """ signals = ['highlight'] def __init__(self, text, filename=None, lexer=None, numbering=False, *args, **kwargs): self._lexer = lexer self.filename = filename self.numbering = numbering super(SourceText, self).__init__(text, *args, **kwargs) signals.connect_signal(self, 'highlight', self._highlight) def get_lexer(self): """Return the current source highlighting lexer""" return self._lexer def update_lexer(self, lexer=None): """ Update source highlighting lexer using the given one or by inspecting filename or text content if ``lexer`` is None. :note: Require pygments, else do nothing. """ if not pygments: return if not self.text: return text = self.text if lexer is None and self.filename: # try to get lexer from filename try: lexer = lexers.get_lexer_for_filename(self.filename, text) except (ClassNotFound, TypeError): #TypeError: pygments is confused pass if lexer is None and text: # try to get lexer from text try: lexer = lexers.guess_lexer(text) except (ClassNotFound, TypeError): #TypeError: pygments is confused pass self._lexer = lexer if lexer == None: # No lexer found => finish return # reduce "lag" while rendering the text as pygments may take a while to # highlight the text. So we colorize only the first part of the text # and delay coloring the full text. The 3000st chars seems good on my # laptop :) signals.delay_emit_signal(self, 'highlight', 0.05, self.text) colored = list(lex(self.text[:3000], self._lexer)) #remove the f*@!king \n added by lex colored[-1] = (colored[-1][0], colored[-1][1][:-1]) self.set_text(colored + [self.text[3000:]]) def _highlight(self, text): self.set_text(list(lex(text, self._lexer))) def clear_lexer(self): """Disable source highlighting""" self.set_text(self.text) lexer = property(get_lexer, update_lexer, clear_lexer, 'source highlighting lexer (require pygments)') def render(self, size, focus=False): """ Render contents with wrapping, alignment and line numbers. """ (maxcol,) = size text, attr = self.get_text() trans = self.get_line_translation(maxcol, (text, attr)) return apply_text_layout(text, attr, trans, maxcol, numbering=self.numbering) hgview-1.9.0/hgviewlib/curses/mainframe.py0000644000015700001640000002430212607505500021376 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ Module that contains the curses main frame, using urwid, that mimics the vim/emacs interface. +------------------------------------------------+ | | | | | body | | | | | +------------------------------------------------+ | banner | +------------------------------------------------+ | footer | +------------------------------------------------+ * *body* display the main contant * *banner* display some short information on the current program state * *footer* display program logs and it is used as input area """ import urwid import logging import urwid.raw_display from urwid.signals import connect_signal, emit_signal from hgviewlib.curses import helpviewer from hgviewlib.curses import (CommandArg as CA, help_command, register_command, unregister_command, emit_command, connect_command, complete_command, hg_command_map, History) def quitall(): """ usage: quitall Quit the program """ raise urwid.ExitMainLoop() def close(mainframe): """ Close the current buffer """ try: mainframe.pop() except StopIteration: # last body => quit program quitall() class MainFrame(urwid.Frame): """Main console frame that mimic the vim interface. You shall *register_commands* at startup then *unregister_commands* at end. """ def __init__(self, name, body, *args, **kwargs): footer = Footer() self._bodies = {name:body} self._visible = name super(MainFrame, self).__init__(body=body, header=None, footer=footer, *args, **kwargs) connect_signal(footer, 'end command', lambda status: self.set_focus('body')) def register_commands(self): """Register specific command""" register_command(('quit','q'), 'Close the current pane.') register_command(('quitall', 'qa'), 'Quit the program.') register_command(('refresh', 'r'), 'Refresh the display') register_command(('help', 'h'), 'Show the help massage.', CA('command', str, ('command name for which to display the help. ' 'Display the global help if omitted.'))) connect_command('quit', close, args=(self,)) connect_command('quitall', quitall) connect_command('help', self.show_command_help) self.body.register_commands() def unregister_commands(self): """unregister specific commands""" unregister_command('quit') unregister_command('q') unregister_command('quitall') unregister_command('qa') unregister_command('help') unregister_command('h') self.body.unregister_commands() def _get_visible(self): """return the name of the current visible body""" return self._visible def _set_visible(self, name): """modify the visible body giving its name""" self._visible = name self.body = self._bodies[self._visible] visible = property(_get_visible, _set_visible, None, 'name of the visible body') def add(self, name, body): """Add a body to the mainframe and focus on it""" self._bodies[name] = body self.visible = name def pop(self, name=None): """Remove and return a body (default to current). Then focus on the last available or raise StopIteration.""" if name is None: name = self.visible ret = self._bodies.pop(name) self.visible = self._bodies.__iter__().next() return ret def __contains__(self, name): """a.__contains__(b) <=> b in a Return True if `name` corresponds to a managed body """ return name in self._bodies def keypress(self, size, key): """allow subclasses to intercept keystrokes""" key = super(MainFrame, self).keypress(size, key) if key is None: return if hg_command_map[key] == 'command key': emit_signal(self.footer, 'start command', key) self.set_focus('footer') elif hg_command_map[key] == 'close pane': emit_command('quit') else: cmd = hg_command_map[key] if cmd and cmd[0] == '@': emit_command(hg_command_map[key][1:]) else: return key def show_command_help(self, command=None): """ usage: edit [command] Show the help massage of the ``command``. :command: a command name for which to display the help. If omitted, the overall program help is displayed. """ doc = None if command: logging.info(help_command(command)) else: helpbody = helpviewer.HelpViewer(doc) helpbody.title = 'Main help' self.add('help', helpbody) logging.info('":q" to quit.') # better name for header as we use it as banner banner = property(urwid.Frame.get_header, urwid.Frame.set_header, None, 'banner widget') class Footer(urwid.AttrWrap): """Footer widget used to display message and for inputs. """ signals = ['start command', 'end command'] def __init__(self, *args, **kwargs): super(Footer, self).__init__( urwid.Edit('type ":help" for information'), 'INFO', *args, **kwargs) connect_signal(self, 'start command', self.start_command) self.previous_keypress = None self._history = History() self._complete = History() def start_command(self, key): """start looking for user's command""" # just for fun label = {'f5':'command: ', ':':':', 'meta x':'M-x '}[key] self.set('default', label, '') def keypress(self, size, key): "allow subclasses to intercept keystrokes" if hg_command_map[key] == 'validate': self.set('default') cmdline = self.call_command() self._history.append(cmdline) emit_signal(self, 'end command', bool(cmdline)) elif hg_command_map[key] == 'escape': self.set('default', '', '') emit_signal(self, 'end command', False) elif key == 'tab': # hard coded :/ self.complete() elif key == 'up' or key == 'ctrl p': # hard coded :/ self.history(False) elif key == 'down' or key == 'ctrl n': # hard coded :/ self.history(True) else: self.previous_keypress = key return super(Footer, self).keypress(size, key) self.previous_keypress = key def complete(self): """ Lookup for text in the edit area (until the cursor) and complete with available command names (one per call). Calling multiple times consequently will loop over all candidates. """ if self.previous_keypress != 'tab': # hard coded :/ line = self.get_edit_text()[:self.edit_pos] self._complete[:] = History(complete_command(line), line) self._complete.reset_position() if self.complete: self.set_edit_text(self._complete.get_next()) if len(self._complete) == 1: self.set_edit_pos(len(self.edit_text)) def history(self, next=True): """ Recall command from history to the edit area. Calling multiple times consequently will loop over all history entries. """ # keys are hard coded :/ if self.previous_keypress not in ('up', 'down', 'ctrl p', 'ctrl n'): self._history[0] = self.get_edit_text() self._history.reset_position() text = self._history.get_next() if next else self._history.get_prev() self.set_edit_text(text) def set(self, style=None, caption=None, edit=None): '''Set the footer content. :param style: a string that corresponds to a palette entry name :param caption: a string to display in caption :param edit: a string to display in the edit area ''' if style is not None: self.set_attr(style) if caption is not None: self.set_caption(caption) if edit is not None: self.set_edit_text(edit) def call_command(self): ''' Call the command that corresponds to the string given in the edit area ''' cmdline = self.get_edit_text() if not cmdline: self.set('default', '', '') return cmdline = cmdline.strip() if cmdline == '?': cmdline = 'help' elif cmdline.endswith('?'): cmdline = 'help %s' % cmdline[:-1].split(None, 1)[0] elif cmdline.startswith('?'): cmdline = 'help %s' % cmdline[1:].split(None, 1)[0] try: emit_command(cmdline) self.set('INFO') except urwid.ExitMainLoop: # exit, so do not catch this raise except Exception, err: logging.warn(err.__class__.__name__ + ': %s', str(err)) logging.debug('Exception on: "%s"', cmdline, exc_info=True) else: return cmdline hgview-1.9.0/hgviewlib/curses/canvas.py0000644000015700001640000001134712607505500020717 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ Module that contains special canvas features. """ # pylint: disable=W,C,I,R from urwid.canvas import (trim_line, rle_append_modify, apply_target_encoding, rle_len, LayoutSegment, TextCanvas, rle_join_modify) __all__ = ['apply_text_layout'] # hack marks: "#+": added, "#=": modified def apply_text_layout(text, attr, ls, maxcol, numbering=False): #= """ Hack for urwid.canvas.apply_text_layout that able to display line numbers """ if numbering: #+ lnb = len(str(text.count('\n') + 1)) + 1 #+ else: #+ lnb = 0 #+ utext = type(text)==type(u"") t = [] a = [] c = [] class AttrWalk: pass aw = AttrWalk aw.k = 0 # counter for moving through elements of a aw.off = 0 # current offset into text of attr[ak] def arange( start_offs, end_offs ): """Return an attribute list for the range of text specified.""" if start_offs < aw.off: aw.k = 0 aw.off = 0 o = [] while aw.off < end_offs: if len(attr)<=aw.k: # run out of attributes o.append((None,end_offs-max(start_offs,aw.off))) break at,run = attr[aw.k] if aw.off+run <= start_offs: # move forward through attr to find start_offs aw.k += 1 aw.off += run continue if end_offs <= aw.off+run: o.append((at, end_offs-max(start_offs,aw.off))) break o.append((at, aw.off+run-max(start_offs, aw.off))) aw.k += 1 aw.off += run return o for idx, line_layout in enumerate(ls): # trim the line to fit within maxcol line_layout = trim_line( line_layout, text, 0, maxcol - lnb) #= if lnb: #+ line = [str(idx).rjust(lnb - 1) + ' '] #+ linea = [('INFO', lnb)] #+ else: #+ line = [] #= linea = [] #= linec = [] def attrrange( start_offs, end_offs, destw ): """ Add attributes based on attributes between start_offs and end_offs. """ if start_offs == end_offs: [(at,run)] = arange(start_offs,end_offs) rle_append_modify( linea, ( at, destw )) return if destw == end_offs-start_offs: for at, run in arange(start_offs,end_offs): rle_append_modify( linea, ( at, run )) return # encoded version has different width o = start_offs for at, run in arange(start_offs, end_offs): if o+run == end_offs: rle_append_modify( linea, ( at, destw )) return tseg = text[o:o+run] tseg, cs = apply_target_encoding( tseg ) segw = rle_len(cs) rle_append_modify( linea, ( at, segw )) o += run destw -= segw for seg in line_layout: #if seg is None: assert 0, ls s = LayoutSegment(seg) if s.end: tseg, cs = apply_target_encoding( text[s.offs:s.end]) line.append(tseg) attrrange(s.offs, s.end, rle_len(cs)) rle_join_modify( linec, cs ) elif s.text: tseg, cs = apply_target_encoding( s.text ) line.append(tseg) attrrange( s.offs, s.offs, len(tseg) ) rle_join_modify( linec, cs ) elif s.offs: if s.sc: line.append(" "*s.sc) attrrange( s.offs, s.offs, s.sc ) else: line.append(" "*s.sc) linea.append((None, s.sc)) linec.append((None, s.sc)) t.append("".join(line)) a.append(linea) c.append(linec) return TextCanvas(t, a, c, maxcol=maxcol) hgview-1.9.0/hgviewlib/curses/hgrepoviewer.py0000644000015700001640000004440612607505500022154 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ Main curses application for hgview """ try: import pygments from pygments import lexers except ImportError: # pylint: enable=C0103 pygments = None import urwid from urwid import AttrWrap, Pile, Columns, SolidFill, signals from urwid.util import is_mouse_press from mercurial.error import RepoError from hgviewlib.config import HgConfig from hgviewlib.hggraph import HgRepoListWalker from hgviewlib.util import exec_flag_changed, isbfile, tounicode from hgviewlib.curses.exceptions import CommandError from hgviewlib.curses.graphlog import RevisionsWalker from hgviewlib.curses.manifest import ManifestWalker from hgviewlib.curses import (Body, SourceText, ScrollableListBox, register_command, unregister_command, connect_command, emit_command, CommandArg as CA, hg_command_map) class GraphlogViewer(Body): """Graphlog body""" def __init__(self, walker, *args, **kwargs): self.walker = walker self.graphlog_walker = RevisionsWalker(walker=walker) body = ScrollableListBox(self.graphlog_walker) super(GraphlogViewer, self).__init__(body=body, *args, **kwargs) self.title = walker.repo.root signals.connect_signal(self.graphlog_walker, 'focus changed', self.update_title) wc = walker.repo[None] rev = None if not wc.dirty() and wc.p1().rev() >= 0: # parent of working directory is not nullrev rev = wc.p1().rev() self.graphlog_walker.rev = rev def update_title(self, ctx): """update title depending on the given context ``ctx``.""" if ctx.node() is None: hex_ = 'WORKING DIRECTORY' else: hex_ = str(ctx) self.title = '%(root)s [%(hex)s] %(phase)s' % { 'root':self.walker.repo.root, 'hex':hex_, 'phase':ctx.phasestr()} def register_commands(self): '''Register commands and connect commands for bodies''' cnvt = lambda entry: self.walker.repo[entry].rev() register_command( ('goto', 'g'), 'Set focus on a particular revision', CA('revision', cnvt, 'The revision number to focus on (default to last)')) register_command( ('toggle-hidden',), 'Show/hide hidden changesets',) connect_command('toggle-hidden', self.toggle_hidden) self.graphlog_walker.connect_commands() def unregister_commands(self): '''Unregister commands''' unregister_command('goto') unregister_command('g') def toggle_hidden(self, _current=[]): self.walker.show_hidden = not self.walker.show_hidden emit_command('refresh') def render(self, size, focus=True): '''Render the widget. Always use the focus style.''' return super(GraphlogViewer, self).render(size, True) def mouse_event(self, size, event, button, col, row, focus): """Scroll content and show context""" if urwid.util.is_mouse_press(event): if button == 1: emit_command('show-context') return super(GraphlogViewer, self).mouse_event(size, event, button, col, row, True) class ManifestViewer(Body): """Manifest viewer""" def __init__(self, walker, ctx, *args, **kwargs): self.manifest_walker = ManifestWalker(walker=walker, ctx=ctx, manage_description=True, *args, **kwargs) body = ScrollableListBox(self.manifest_walker) super(ManifestViewer, self).__init__(body=body, *args, **kwargs) signals.connect_signal(self.manifest_walker, 'focus changed', self.update_title) self.title = 'Manifest' def update_title(self, filename): '''update the body title.''' tot = len(self.manifest_walker) if self.manifest_walker.focus < 0: self.title = '%i file%s' % (tot, 's' * (tot > 1)) return cur = self.manifest_walker.focus + 1 self.title = '%i/%i [%i%%]' % (cur, tot, cur*100/tot) def render(self, size, focus=True): '''Render the manifest viewer. Always use the focus style.''' return super(ManifestViewer, self).render(size, True) class SourceViewer(Body): """Source Viewer""" signals = ['translated'] def __init__(self, text, *args, **kwargs): self.text = SourceText(text, wrap='clip') self.position = 0 body = ScrollableListBox([self.text]) super(SourceViewer, self).__init__(body=body, *args, **kwargs) def update_position(self, size, isdown): curr, fullheight = self.body.inset_fraction _, displayedheight = size stroke = fullheight - displayedheight if stroke <= 0: # fully displayed self.position = 100 if isdown else 0 else: self.position = max(min(curr * 100 / stroke, 100), 0) signals.emit_signal(self, 'translated') def keypress(self, size, key): super(SourceViewer, self).keypress(size, key) if key.endswith(('up', 'down')): self.update_position(size, key.endswith('down')) def mouse_event(self, size, event, button, col, row, focus): """Scroll content""" if is_mouse_press(event) and button in (4, 5): self.update_position(size) return super(SourceViewer, self).mouse_event(size, event, button, col, row, focus) class ContextViewer(Columns): """Context viewer (manifest and source)""" signals = ['update source title'] MANIFEST_SIZE = 0.3 def __init__(self, walker, *args, **kwargs): self._walker = walker self._filename = None self._size_cache = (0, 0) self.cfg = HgConfig(walker.repo.ui) self.manifest = ManifestViewer(walker=walker, ctx=None) self.manifest_walker = self.manifest.manifest_walker self.source = SourceViewer('') self.source_text = self.source.text self._source_title_cache = '' widget_list = [('weight', 1 - self.MANIFEST_SIZE, self.source), ('fixed', 1, AttrWrap(SolidFill(' '), 'banner')), ('weight', self.MANIFEST_SIZE, self.manifest), ] super(ContextViewer, self).__init__(widget_list=widget_list, *args, **kwargs) signals.connect_signal(self.manifest_walker, 'focus changed', self.update_source) signals.connect_signal(self, 'update source title', self.update_source_title_cache) signals.connect_signal(self.source, 'translated', self.update_source_title) def register_commands(self): """Register commands and commands of bodies""" register_command('set-max-file-size', 'max size of handled file for diff computation, and so on.', CA('size', int, 'octets (-1 means no max size)')) connect_command('set-max-file-size', self.modify_max_file_size) def unregister_commands(self): """Unregister commands and commands of bodies""" unregister_command('set-max-file-size') def modify_max_file_size(self, size): """Modify the max handled file size and update the source content.""" self._walker.graph.maxfilesize = size self.update_source(self._filename) def update_source_title_cache(self, filename, flag): """ Display information about the file in the title of the source body. """ ctx = self.manifest_walker.ctx title = [] if filename is None: title.append(' Description') elif flag == '' or flag == '-': title += [' Removed file: ', ('focus', filename)] else: filectx = ctx.filectx(filename) flag = exec_flag_changed(filectx) if flag: title += [' Exec mode: ', ('focus', flag)] if isbfile(filename): title.append('bfile tracked') renamed = filectx.renamed() if renamed: title += [' Renamed from: ', ('focus', renamed[0])] title += [' File name: ', ('focus', filename)] self._source_title_cache = title self.update_source_title() def update_source_title(self): self.source.title = ['[% 3s%%]' % self.source.position] + self._source_title_cache def update_source(self, filename): """Update the source content.""" ctx = self.manifest_walker.ctx if ctx is None: return self._filename = filename numbering = False flag = '' if filename is None: # source content is the changeset description wrap = 'space' # Do not cut description and wrap content data = tounicode(ctx.description()) if pygments: lexer = lexers.RstLexer() else: # source content is a file wrap = 'clip' # truncate lines flag, data = self.manifest_walker.filedata(filename) lexer = None # default to inspect filename and/or content if flag == '=' and pygments: # modified => display diff lexer = lexers.DiffLexer() if flag == '=' else None elif flag == '-' or flag == '': # removed => just say it if pygments: lexer = lexers.DiffLexer() data = '- Removed file' elif flag == '+': # Added => display content numbering = True lexer = None signals.delay_emit_signal(self, 'update source title', 0.05, filename, flag) self.source_text.set_wrap_mode(wrap) self.source_text.set_text(data or '') if pygments: self.source_text.lexer = lexer self.source_text.numbering = numbering self.source.body.set_focus_valign('top') # reset offset self.source.position = 0 def keypress(self, size, key): self._size_cache = size try: self._keypress(hg_command_map[key]) except CommandError: return key def _keypress(self, command): "allow subclasses to intercept keystrokes" widths = self.column_widths(self._size_cache) maxrow = self._size_cache[1] if command.startswith('source'): self._previous_source_position = self.source.position if command == 'manifest up': _size = widths[2], maxrow self.manifest.keypress(_size, 'up') elif command == 'manifest down': _size = widths[2], maxrow self.manifest.keypress(_size, 'down') if command == 'source up': _size = widths[0], maxrow self.source.keypress(_size, 'up') elif command == 'source down': _size = widths[0], maxrow self.source.keypress(_size, 'down') elif command == 'manifest page up': _size = widths[2], maxrow self.manifest.keypress(_size, 'page up') elif command == 'manifest page down': _size = widths[2], maxrow self.manifest.keypress(_size, 'page down') if command == 'source page up': _size = widths[0], maxrow self.source.keypress(_size, 'page up') elif command == 'source page down': _size = widths[0], maxrow self.source.keypress(_size, 'page down') else: CommandError('unknown command: %r' % command) def clear(self): """Clear content""" self.manifest_walker.clear() self.source_text.set_text('') class RepoViewer(Pile): """Repository viewer (graphlog and context)""" CONTEXT_SIZE = 0.5 def __init__(self, repo, *args, **kwargs): if repo.root is None: raise RepoError("There is no Mercurial repository here (.hg not found)!") self.repo = repo self.cfg = HgConfig(repo.ui) self._show_context = 0 # O:hide, 1:half, 2:maximized self.refreshing = False # flag to now if the repo is refreshing self._walker = HgRepoListWalker(repo) self.graphlog = GraphlogViewer(walker=self._walker) self.context = ContextViewer(walker=self._walker) widget_list = [('weight', 1 - self.CONTEXT_SIZE, self.graphlog),] super(RepoViewer, self).__init__(widget_list=widget_list, focus_item=0, *args, **kwargs) if self.cfg.getContentAtStartUp(): self.show_context() def update_context(self, ctx): """Change the current displayed context""" self.context.manifest_walker.set_ctx(ctx, reset_focus=(not self.refreshing)) def register_commands(self): """Register commands and commands of bodies""" register_command('hide-context', 'Hide context pane.') register_command('show-context', 'Show context pane.', CA('height', float, 'Relative height [0-1] of the context pane.')) register_command('maximize-context', 'Maximize context pane.') self.graphlog.register_commands() self.context.register_commands() connect_command('hide-context', self.hide_context) connect_command('show-context', self.show_context) connect_command('maximize-context', self.maximize_context) connect_command('refresh', self.refresh) def unregister_commands(self): """Unregister commands and commands of bodies""" self.graphlog.unregister_commands() self.context.unregister_commands() def refresh(self): graphlog_walker = self.graphlog.graphlog_walker manifest_walker = self.context.manifest_walker self.refreshing = True rev = graphlog_walker.rev filename = manifest_walker.filename self._walker.setRepo() try: graphlog_walker.set_rev(rev) # => focus changed => update_context except AttributeError: # rev stripped graphlog_walker.rev = None manifest_walker.filename = filename self.refreshing = False def hide_context(self): ''' hide the context widget''' if self._show_context == 0: # already hidden return self._deactivate_context() self.item_types[:] = [('weight', 1)] self.widget_list[:] = [self.graphlog] self._show_context = 0 def maximize_context(self): '''hide the graphlog widget''' if self._show_context == 2: # already maximized return self._activate_context() self.item_types[:] = [('weight', 1)] self.widget_list[:] = [self.context] self._show_context = 2 def show_context(self, height=None): '''show context and graphlog widgets''' if self._show_context == 1: # already half return self._activate_context() if height is None: height = self.CONTEXT_SIZE self.item_types[:] = [('weight', 1 - height), ('weight', height),] self.widget_list[:] = [self.graphlog, self.context] self._show_context = 1 def _activate_context(self): context_walker = self.context.manifest_walker graphlog_ctx = self.graphlog.graphlog_walker.get_ctx() if context_walker.ctx != graphlog_ctx: self.update_context(graphlog_ctx) signals.connect_signal(self.graphlog.graphlog_walker, 'focus changed', self.update_context) def _deactivate_context(self): signals.disconnect_signal(self.graphlog.graphlog_walker, 'focus changed', self.update_context) def keypress(self, size, key): "allow subclasses to intercept keystrokes" if self._show_context == 0 and hg_command_map[key] == 'validate': self.show_context() return if hg_command_map[key] == 'close pane' and self._show_context > 0: # allows others to catch 'close pane' self.hide_context() return if self._show_context < 2: if hg_command_map[key] == 'graphlog up': _size = self.get_item_size(size, 0, True) self.graphlog.keypress(_size, 'up') return if hg_command_map[key] == 'graphlog down': _size = self.get_item_size(size, 0, True) self.graphlog.keypress(_size, 'down') return if hg_command_map[key] == 'graphlog page up': _size = self.get_item_size(size, 0, True) self.graphlog.keypress(_size, 'page up') return if hg_command_map[key] == 'graphlog page down': _size = self.get_item_size(size, 0, True) self.graphlog.keypress(_size, 'page down') return if self._show_context > 0: idx = 1 if self._show_context == 1 else 0 _size = self.get_item_size(size, idx, True) return self.context.keypress(_size, key) return key def mouse_event(self, size, event, button, col, row, focus): """Hide context""" if urwid.util.is_mouse_press(event): if button == 3: emit_command('hide-context') return return super(RepoViewer, self).mouse_event(size, event, button, col, row, focus) hgview-1.9.0/hgviewlib/__pkginfo__.py0000644000015700001640000000436612607505500020374 0ustar narvalnarval00000000000000# pylint: disable=W0622 # coding: iso-8859-1 # 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, see . """Copyright (c) 2000-2012 LOGILAB S.A. (Paris, FRANCE). http://www.logilab.fr/ -- mailto:contact@logilab.fr """ import glob from os.path import join as joinpath distname = modname = 'hgview' numversion = (1, 9, 0) version = '.'.join([str(num) for num in numversion]) license = 'GPL' copyright = '''Copyright 2007-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. http://www.logilab.fr/ -- mailto:contact@logilab.fr''' classifiers = ['Development Status :: 4 - Beta', 'Environment :: X11 Applications :: Qt', 'Environment :: Win32 (MS Windows)', 'Environment :: MacOS X', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Software Development :: Version Control', ] description = "a Mercurial interactive history viewer" author = "Logilab" author_email = 'python-projects@lists.logilab.org' # TODO - publish web = "http://www.logilab.org/projects/%s" % modname ftp = "ftp://ftp.logilab.org/pub/%s" % modname mailinglist = "mailto://python-projects@lists.logilab.org" scripts = ['bin/hgview'] debian_name = 'hgview' debian_maintainer = 'Alexandre Fayolle' debian_maintainer_email = 'afayolle@debian.org' pyversions = ["2.5"] debian_handler = 'python-dep-standalone' include_dirs = [] data_files = [ [joinpath('share', 'doc', 'hgview', 'examples'), [joinpath('hgviewlib', 'qt4', 'resources', 'description.css')]] ] hgview-1.9.0/hgviewlib/config.py0000644000015700001640000003045412607505500017405 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . # # pylint: disable=C0103 """ Module for managing configuration parameters of hgview using Hg's configuration system """ from functools import partial import os import re import shlex def cached(meth): """ decorator to cache config values once they are read """ name = meth.func_name def wrapper(self, *args, **kw): if name in self._cache: return self._cache[name] res = meth(self, *args, **kw) self._cache[name] = res return res wrapper.__doc__ = meth.__doc__ return wrapper class HgConfig(object): """ Class managing user configuration from hg standard configuration system (.hgrc) """ def __init__(self, ui, section="hgview"): self.ui = ui self.section = section self._cache = {} def _fromconfig(self, name, default, configmethod='config'): '''allow per-interface configuration. look for ``interface.config`` then for ``config`` if the first were not found''' getconfig = getattr(self.ui, configmethod) out = getconfig(self.section, '.'.join((self.ui.opts.interface, name)), None) if out is not None: return out return getconfig(self.section, name, default) @cached def getFancyReplace(self): r""" fancyreplace: ``"patt" "repl"`` used to modify description by replacing ``patt`` by ``repl`` using regular expression (``re.sub`` for instance). Ex: "#(\d+)":"`#\\1 `_" """ data = self._fromconfig('fancyreplace', None) if data is None: return data data = shlex.split(data) assert len(data) == 2 patt, repl = data return partial(re.compile(patt).sub, repl) @cached def getFont(self, default='Monospace'): """ font: default font used to display diffs and files. Use Qt4 format. """ return self._fromconfig('font', default) @cached def getFontSize(self, default=9): """ fontsize: text size in file content viewer """ return int(self._fromconfig('fontsize', default)) @cached def getDescriptionStylePath(self, default=None): """ descriptionstylepath: stylesheet file path used to format the revision description. You should found a copy of the default style sheet in the documentation files location of your system (ex. /usr/share/doc/hgview/examples). """ return self._fromconfig('descriptionstylepath', default) @cached def getDotRadius(self, default=8): """ dotradius: radius (in pixels) of the dot in the revision graph """ return int(self._fromconfig('dotradius', default)) @cached def getUsers(self): """ users: path of the file holding users configurations """ users = {} aliases = {} usersfile = self._fromconfig('users', os.path.join('~', ".hgusers")) cfgfile = None if usersfile: try: cfgfile = open(os.path.expanduser(usersfile)) except IOError: cfgfile = None if cfgfile: currid = None for line in cfgfile: line = line.strip() if not line or line.startswith('#'): continue cmd, val = line.split('=', 1) if cmd == 'id': currid = val if currid in users: print "W: user %s is defined several times" % currid users[currid] = {'aliases': set()} elif cmd == "alias": users[currid]['aliases'].add(val) if val in aliases: print ("W: alias %s is used in several " "user definitions" % val) aliases[val] = currid else: users[currid][cmd] = val return users, aliases @cached def getFileDescriptionView(self, default='persistent'): """ descriptionview: :asfile: compact view with changeset description in the file list :persistent: persistent view with changeset description always visible (default) """ return self._fromconfig('descriptionview', default).lower() @cached def getFileDescriptionColor(self, default='magenta'): """ filedescriptioncolor: display color of the "description" entry """ return self._fromconfig('filedescriptioncolor', default) @cached def getFileModifiedColor(self, default='blue'): """ filemodifiedcolor: display color of a modified file """ return self._fromconfig('filemodifiedcolor', default) @cached def getFileRemovedColor(self, default='red'): """ fileremovedcolor: display color of a removed file """ return self._fromconfig('fileremovededcolor', default) @cached def getFileDeletedColor(self, default='darkred'): """ filedeletedcolor: display color of a deleted file """ return self._fromconfig('filedeletedcolor', default) @cached def getFileAddedColor(self, default='green'): """ fileaddedcolor: display color of an added file """ return self._fromconfig('fileaddedcolor', default) @cached def getRowHeight(self, default=20): """ rowheight: height (in pixels) on a row of the revision table """ return int(self._fromconfig('rowheight', default)) @cached def getHideFindDelay(self, default=10000): """ hidefinddelay: delay (in ms) after which the find bar will disappear """ return int(self._fromconfig('hidefindddelay', default)) @cached def getFillingStep(self, default=300): """ fillingstep: number of nodes 'loaded' at a time when updating repo graph log """ return int(self._fromconfig('fillingstep', default)) @cached def getChangelogColumns(self, default=None): """ changelogcolumns: ordered list of displayed columns in changelog views; defaults to ID, Branch, Log, Author, Date """ cols = self._fromconfig('changelogcolumns', default) if cols is None: return None return [col.strip() for col in cols.split(',') if col.strip()] @cached def getFilelogColumns(self, default=None): """ filelogcolumns: ordered list of displayed columns in filelog views; defaults to ID, Log, Author, Date """ cols = self._fromconfig('filelogcolumns', default) if cols is None: return None return [col.strip() for col in cols.split(',') if col.strip()] @cached def getDisplayDiffStats(self, default="yes"): """ displaydiffstats: flag controlling the appearance of the 'Diff' column in a revision's file list """ val = str(self._fromconfig('displaydiffstats', default)) return val.lower() in ['true', 'yes', '1', 'on'] @cached def getMaxFileSize(self, default=100000): """ maxfilesize: max size of a file for diff computations, display content, etc. (-1 means no max size) """ return int(self._fromconfig('maxfilesize', default)) @cached def getDiffBGColor(self, default='black'): """ diffbgcolor: background color of diffs """ return self._fromconfig('diffbgcolor', default) @cached def getDiffFGColor(self, default='white'): """ difffgcolor: text color of diffs """ return self._fromconfig('difffgcolor', default) @cached def getDiffPlusColor(self, default='green'): """ diffpluscolor: text color of added lines in diffs """ return self._fromconfig('diffpluscolor', default) @cached def getDiffMinusColor(self, default='red'): """ diffminuscolor: text color of removed lines in diffs """ return self._fromconfig('diffminuscolor', default) @cached def getDiffSectionColor(self, default='magenta'): """ diffsectioncolor: text color of new section in diffs """ return self._fromconfig('diffsectioncolor', default) @cached def getMQFGColor(self, default='#ff8183'): """ mqfgcolor: bg color to highlight mq patches """ return self._fromconfig('mqfgcolor', default) @cached def getMQHideTags(self, default=False): """ mqhidetags: hide mq tags """ return self._fromconfig('mqhidetags', default) @cached def getContentAtStartUp(self, default=True): """ contentatstartup: show the content of changeset at startup (bottom part) """ return bool(self._fromconfig('contentatstartup', default, configmethod='configbool')) @cached def getShowHidden(self, default=False): """ showhidden: show hidden changeset at startup """ return bool(self._fromconfig('showhidden', default, configmethod='configbool')) @cached def getInterface(self, default=None): """ interface: which GUI interface to use (among "qt", "raw" and "curses") """ return self.ui.config(self.section, 'interface', default) @cached def getNonPublicOnTop(self, default=False): """ nonpublicontop: display non public changesets on top of the graph log (disabled with *show hidden*) """ return bool(self._fromconfig('nonpublicontop', default, configmethod='configbool')) @cached def getShowObsolete(self, default=True): """ showobsolete: display obsolete relations """ return bool(self._fromconfig('showobsolete', default, configmethod='configbool')) @cached def getExportTemplate(self): """ exporttemplate: template used to serialize changeset metadata while exporting into the window manager clipboard. (default to `ui.logtemplate`) """ return self._fromconfig('exporttemplate', None) or \ self.ui.config('ui', 'logtemplate') _HgConfig = HgConfig # HgConfig is instantiated only once (singleton) # # this 'factory' is used to manage this (not using heavy guns of # metaclass or so) _hgconfig = None def HgConfig(ui): """Factory to instantiate HgConfig class as a singleton """ # pylint: disable=E0102 global _hgconfig if _hgconfig is None: _hgconfig = _HgConfig(ui) return _hgconfig def get_option_descriptions(rest=False): """ Extract options descriptions (docstrings of HgConfig methods) """ options = [] for attr in dir(_HgConfig): if attr.startswith('get'): meth = getattr(_HgConfig, attr) if callable(meth): doc = meth.__doc__ if doc and doc.strip(): doc = doc.strip() if rest: doc = re.sub(r' *(?P.*) *: *(?P.*)', r'``\1`` \2', doc.strip()) doc = ' '.join(doc.split()) # remove \n and other multiple whitespaces options.append(doc) return options hgview-1.9.0/hgviewlib/application.py0000644000015700001640000001626012607505500020442 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2003-2012 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, see . """ Application utilities. """ import os, sys, traceback from optparse import OptionParser from mercurial import hg, ui as uimod from mercurial.error import RepoError from hgviewlib import __pkginfo__ from hgviewlib.util import find_repository, rootpath, build_repo from hgviewlib.config import HgConfig class NullRepo(object): """Placeholder repository""" ui = uimod.ui() root = None class Viewer(object): """Base viewer class interface.""" def __init__(self, *args, **kwargs): raise NotImplementedError( 'This feature has not yet been implemented. Coming soon ...') class FileViewer(Viewer): """Single file revision graph viewer.""" def __init__(self, repo, filename, **kwargs): super(FileViewer, self).__init__(**kwargs) class FileDiffViewer(Viewer): """Viewer that displays diffs between different revisions of a file.""" def __init__(self, repo, filename, **kwargs): super(FileDiffViewer, self).__init__(**kwargs) class HgRepoViewer(Viewer): """hg repository viewer.""" def __init__(self, repo, **kwargs): super(HgRepoViewer, self).__init__(**kwargs) class ManifestViewer(Viewer): """Viewer that displays all files of a repo at a given revision""" def __init__(self, repo, rev, **kwargs): super(ManifestViewer, self).__init__(**kwargs) class ApplicationError(ValueError): """Exception that may occur while launching the application""" class HgViewApplication(object): # class that must be instantiated FileViewer = FileViewer FileDiffViewer = FileDiffViewer HgRepoViewer = HgRepoViewer ManifestViewer = ManifestViewer def __init__(self, repo, opts, args, **kawrgs): self.viewer = None if opts.navigate and len(args) != 1: ApplicationError( "you must provide a filename to start in navigate mode") if len(args) > 1: ApplicationError("provide at most one file name") self.opts = opts self.args = args self.repo = repo self.choose_viewer() def choose_viewer(self): """Choose the right viewer""" if len(self.args) == 1: filename = rootpath(self.repo, self.opts.rev, self.args[0]) if not filename: raise ApplicationError("%s is not a tracked file" % self.args[0]) # should be a filename of a file managed in the repo if self.opts.navigate: viewer = self.FileViewer(self.repo, filename) else: viewer = self.FileDiffViewer(self.repo, filename) else: rev = self.opts.rev if rev: try: self.repo.changectx(rev) except RepoError, e: raise ApplicationError("Cannot find revision %s" % rev) else: viewer = self.ManifestViewer(self.repo, rev) else: viewer = self.HgRepoViewer(self.repo) self.viewer = viewer def exec_(self): raise NotImplementedError() def _qt_application(): from hgviewlib.qt4.application import HgViewQtApplication as Application return Application def _curses_application(): from hgviewlib.curses.application import HgViewUrwidApplication as Application return Application LOADERS = {'qt': _qt_application, 'raw': _curses_application, 'curses': _curses_application} def start(repo, opts, args, fnerror): """ start hgview """ config = HgConfig(repo.ui) repo.ui.opts = opts # pick the interface to use inter = opts.interface if not inter: inter = config.getInterface() if inter is None: interfaces = ['qt'] if os.name != 'nt': # if we are not on Windows try terms fallback interfaces.append('raw') elif inter == 'qt': interfaces = ['qt'] elif inter in ('raw', 'curses'): interfaces = [inter] else: fnerror('Unknown interface: %s' % inter) return 1 # initialize possible interface errors = [] Application = None for inter in interfaces: try: Application = LOADERS[inter]() except ImportError: # we store full exception context to allow --traceback option # to print a proper traceback. errors.append((inter, sys.exc_info())) else: opts.interface = inter break else: for inter, err in errors: if '--traceback' in sys.argv: traceback.print_exception(*err) fnerror('Interface %s is not available: %s' % (inter, err[1])) return 2 # actually launch the application try: app = Application(repo, opts, args) except (ApplicationError, NotImplementedError), err: fnerror(str(err)) return app.exec_() def main(): """ Main application entry point. """ usage = '''%prog [options] [filename] Starts a visual hg repository navigator. - With no options, starts the main repository navigator. - If a filename is given, starts in filelog diff mode (or in filelog navigation mode if -n option is set). - With -r option, starts in manifest viewer mode for given revision. ''' parser = OptionParser(usage, version=__pkginfo__.version) parser.add_option('-I', '--interface', dest='interface', help=('which GUI interface to use (among "qt", "raw"' ' and "curses")'), ) parser.add_option('-R', '--repository', dest='repo', help='location of the repository to explore') parser.add_option('-r', '--rev', dest='rev', default=None, help='start in manifest navigation mode at rev R') parser.add_option('-n', '--navigate', dest='navigate', default=False, action="store_true", help='(with filename) start in navigation mode') opts, args = parser.parse_args() if opts.repo: dir_ = opts.repo else: dir_ = os.getcwd() repopath = find_repository(dir_) try: if repopath: u = uimod.ui() repo = build_repo(u, repopath) else: repo = NullRepo() try: sys.exit(start(repo, opts, args, parser.error)) except KeyboardInterrupt: print 'interrupted!' except RepoError, e: parser.error(e) hgview-1.9.0/hgviewlib/util.py0000644000015700001640000002212612607505500017112 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- # util functions # # 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. """ Several helper functions """ import os import os.path as osp import string from functools import partial from mercurial import hg, config, error from hgviewlib.hgpatches.scmutil import match from hgviewlib.hgpatches import precursorsmarkers, successorsmarkers def tounicode(text): """ Tries to convert ``text`` into a unicode string. If ``text`` is already a unicode object return it. """ if isinstance(text, unicode): return text else: text = str(text) for encoding in ('utf-8', 'iso-8859-15', 'cp1252'): try: return text.decode(encoding) except (UnicodeError, UnicodeDecodeError): pass return text.decode('utf-8', 'replace') def isexec(filectx): """ Return True is the file at filectx revision is executable """ if hasattr(filectx, "isexec"): return filectx.isexec() return "x" in filectx.flags() def exec_flag_changed(filectx): """ Return True if the file referenced by filectx has changed its exec flag """ flag = isexec(filectx) parents = filectx.parents() if not parents: return "" pflag = isexec(parents[0]) if flag != pflag: if flag: return "set" else: return "unset" return "" def isbfile(filename): return filename and filename.startswith('.hgbfiles' + os.sep) def bfilepath(filename): return filename and filename.replace('.hgbfiles' + os.sep, '') def find_repository(path): """returns 's mercurial repository None if is not under hg control """ path = os.path.abspath(path) while not os.path.isdir(os.path.join(path, ".hg")): oldpath = path path = os.path.dirname(path) if path == oldpath: return None return path def rootpath(repo, rev, path): """return the path name of 'path' relative to repo's root at revision rev; path is relative to cwd """ ctx = repo[rev] filenames = list(ctx.walk(match(ctx, [path], {}))) if len(filenames) != 1 or filenames[0] not in ctx.manifest(): return None else: return filenames[0] # XXX duplicates logilab.mtconverter.__init__ code CONTROL_CHARS = [chr(ci) for ci in range(32)] TR_CONTROL_CHARS = [' '] * len(CONTROL_CHARS) for c in ('\n', '\r', '\t'): TR_CONTROL_CHARS[ord(c)] = c TR_CONTROL_CHARS[ord('\f')] = '\n' TR_CONTROL_CHARS[ord('\v')] = '\n' ESC_CAR_TABLE = string.maketrans(''.join(CONTROL_CHARS), ''.join(TR_CONTROL_CHARS)) ESC_UCAR_TABLE = unicode(ESC_CAR_TABLE, 'latin1') def xml_escape(data): """escapes XML forbidden characters in attributes and CDATA""" if isinstance(data, unicode): data = data.translate(ESC_UCAR_TABLE) else: data = data.translate(ESC_CAR_TABLE) return (data.replace('&','&').replace('<','<').replace('>','>') .replace('"','"').replace("'",''')) def format_desc(desc, width): """ format a ctx description for oneliner representation (summary view) """ udesc = tounicode(desc) udesc = xml_escape(udesc.split('\n', 1)[0]) if len(udesc) > width: udesc = udesc[:width] + '...' return udesc def allbranches(repo, include_closed=False): """Return the list of branches in a repo Closed branch are excluded unless `include_closed=True`.""" if getattr(repo, 'branchtags', None) is None: # mercurial 2.9 and above. branchtags is gone but we have cached value # to know if a branch is closed or not. clbranches = [] branches = [] for tag, heads, tip, closed in repo.branchmap().iterbranches(): if closed: clbranches.append(tag) else: branches.append(tag) else: allbranches = sorted(repo.branchtags().items()) openbr = [] for branch, brnode in allbranches: openbr.extend(repo.branchheads(branch, closed=False)) clbranches = [br for br, node in allbranches if node not in openbr] branches = [br for br, node in allbranches if node in openbr] if include_closed: branches = branches + clbranches return branches def first_known_precursors(ctx, excluded=()): obsstore = getattr(ctx._repo, 'obsstore', None) startnode = ctx.node() nm = ctx._repo.changelog.nodemap if obsstore is not None: markers = precursorsmarkers(obsstore, startnode) # consider all precursors candidates = set(mark[0] for mark in markers) seen = set(candidates) if startnode in candidates: candidates.remove(startnode) else: seen.add(startnode) while candidates: current = candidates.pop() # is this changeset in the displayed set ? crev = nm.get(current) if crev is not None and crev not in excluded: yield ctx._repo[crev] else: for mark in precursorsmarkers(obsstore, current): if mark[0] not in seen: candidates.add(mark[0]) seen.add(mark[0]) def first_known_successors(ctx, excluded=()): obsstore = getattr(ctx._repo, 'obsstore', None) startnode = ctx.node() nm = ctx._repo.changelog.nodemap if obsstore is not None: markers = successorsmarkers(obsstore, startnode) # consider all precursors candidates = set() for mark in markers: candidates.update(mark[1]) seen = set(candidates) if startnode in candidates: candidates.remove(startnode) else: seen.add(startnode) while candidates: current = candidates.pop() # is this changeset in the displayed set ? crev = nm.get(current) if crev is not None and crev not in excluded: yield ctx._repo[crev] else: for mark in successorsmarkers(obsstore, current): for succ in mark[1]: if succ not in seen: candidates.add(succ) seen.add(succ) def build_repo(ui, path): """build a repo like hg.repository But ensure it is not filtered whatever the version used""" if isinstance(path, unicode): path = path.encode('utf-8') repo = hg.repository(ui, path) return getattr(repo, 'unfiltered', lambda: repo)() def upward_path(path): """A generator function that upward path >>> for path in upward_path('/tmp/jungle/elephants/babar/head/'): ... print path ... /tmp/jungle/elephants/babar/head /tmp/jungle/elephants/babar /tmp/jungle/elephants /tmp/jungle /tmp / """ path = path.rstrip(os.path.sep) yield path while path != osp.dirname(path): # root folder reached? path = osp.dirname(path) yield osp.normpath(path) def _get_conf(repo_path, conf_file): """Read a configuratio inside a repo""" # We do not ask to the extension api (there is no public # api). Because setting up hg to use the extension is merely more # complicated than a naive approach. # they all have the same kind of settings: a file on the root # folder in rc-style. confpath = osp.join(repo_path, conf_file) if not osp.exists(confpath): return None conf = config.config() try: conf.read(confpath) out = conf except error.ParseError: out = None return out def _get_subrepo(repo_path): """Get subrepo style nested repo""" config = _get_conf(repo_path, '.hgsub') if config is None: return None return ((path, path) for path in config[''].keys()) def _get_guestrepo(repo_path): """Get guestrepo style nested repo""" config = _get_conf(repo_path, '.hgguestrepo') if config is None: return None return ((value.split(None, 1)[0], path) for path, value in config[''].iteritems()) SUBREPO_GETTERS = [_get_subrepo, _get_guestrepo] def read_nested_repo_paths(repopath): '''Return a list of pairs ``(name, path)``. They describe sub-repositories managed by *subrepo*, and *guestrepo* within the master repository located at ``repopath``''' repopath = osp.abspath(repopath) subpath = partial(osp.join, repopath) for helper in SUBREPO_GETTERS: data = helper(repopath) if data is not None: return [(name, osp.normpath(subpath(path))) for (name, path) in data] return [] def compose(f, g): """Return the functions composition ``f o g``. >>> foo = compose(str, float) >>> foo(1) '1.0' """ composed = lambda *args, **kwargs: f(g(*args, **kwargs)) names = (getattr(f, '__name__', 'unknown'), getattr(g, '__name__', 'unknown')) composed.__doc__ = 'functions composition (%s o %s)' % names composed.__name__ = '(%s o %s)' % names return composed hgview-1.9.0/hgviewlib/__init__.py0000644000015700001640000000214212607505500017670 0ustar narvalnarval00000000000000# Copyright (c) 2003-2012 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, see . """mercurial interactive history viewer Its purpose is similar to the hgk tool of mercurial, and it has been written with efficiency in mind when dealing with big repositories (it can happily be used to browse Linux kernel source code repository). """ from mercurial import demandimport # this should help docutils, see hgrepoview.rst2html demandimport.ignore.append('roman') hgview-1.9.0/hgviewlib/decorators.py0000644000015700001640000000100212607505500020270 0ustar narvalnarval00000000000000# -*- coding: utf-8 -*- """ Some useful decorator functions """ import time def timeit(func): """Decorator used to time the execution of a function""" def timefunc(*args, **kwargs): """wrapper""" t_1 = time.time() t_2 = time.clock() res = func(*args, **kwargs) t_3 = time.clock() t_4 = time.time() print "%s: %.2fms (time) %.2fms (clock)" % \ (func.func_name, 1000*(t_3 - t_2), 1000*(t_4 - t_1)) return res return timefunc hgview-1.9.0/setup.py0000644000015700001640000003416712607505500015325 0ustar narvalnarval00000000000000#!/usr/bin/env python # pylint: disable=W0142,W0403,W0404,E0611,W0613,W0622,W0622,W0704 # pylint: disable=R0904,C0103 # # Copyright (c) 2003 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, see . """ Generic Setup script, takes package info from hgviewlib.__pkginfo__.py file """ from __future__ import nested_scopes, with_statement import os import sys import shutil from os.path import isdir, exists, join, walk, splitext, basename, dirname from subprocess import check_call, call as sub_call with_setuptools = False if 'USE_SETUPTOOLS' in os.environ or 'pip' in __file__: try: from setuptools import setup from setuptools.command.install import install as _install from setuptools.command.build_py import build_py as _build_py from setuptools.command.install_lib import install_lib with_setuptools = True except: with_setuptools = False if with_setuptools is False: import warnings from distutils.command.install import install as _install from distutils.core import setup from distutils.command.build_py import build_py as _build_py from distutils.command.install_lib import install_lib from distutils.command.build import build as _build from distutils.command.install_data import install_data as _install_data py2exe, innosetup = None, None try: import py2exe import innosetup except ImportError: pass # import required features from hgviewlib.__pkginfo__ import modname, version, license, description, \ web, author, author_email # import optional features try: from hgviewlib.__pkginfo__ import distname except ImportError: distname = modname try: from hgviewlib.__pkginfo__ import scripts except ImportError: scripts = [] try: from hgviewlib.__pkginfo__ import data_files except ImportError: data_files = [] try: from hgviewlib.__pkginfo__ import subpackage_of except ImportError: subpackage_of = None try: from hgviewlib.__pkginfo__ import include_dirs except ImportError: include_dirs = [] try: from hgviewlib.__pkginfo__ import ext_modules except ImportError: ext_modules = None long_description = file('README').read() def setdefaultattr(obj, attrname, value=None): if getattr(obj, attrname, None) is not None: return getattr(obj, attrname) setattr(obj, attrname, value) return value def ensure_scripts(linux_scripts): """ Creates the proper script names required for each platform (taken from 4Suite) """ from distutils import util if util.get_platform()[:3] == 'win': scripts_ = [script + '.bat' for script in linux_scripts] else: scripts_ = linux_scripts return scripts_ class build_qt(_build_py): description = "build every qt related resources (.uic and .qrc and .pyc)" PACKAGE = 'hgviewlib.qt4' def finalize_options(self): _build_py.finalize_options(self) self.packages = ['hgviewlib.qt4'] def compile_src(self, src, dest): compiler = self.get_compiler(src) if not compiler: return dir = os.path.dirname(dest) self.mkpath(dir) sys.stdout.write("compiling %s -> %s\n" % (src, dest)) try: compiler(src, dest) except Exception, e: sys.stderr.write('[Error] %r\n' % str(e)) def run(self): for dirpath, _, filenames in os.walk(self.get_package_dir(self.PACKAGE)): package = dirpath.split(os.sep) for filename in filenames: module = self.get_module_name(filename) module_file = self.get_module_outfile(self.build_lib, package, module) src_file = os.path.join(dirpath, filename) self.compile_src(src_file, module_file) _build_py.run(self) @staticmethod def compile_ui(ui_file, py_file): from PyQt4 import uic with open(py_file, 'w') as fp: uic.compileUi(ui_file, fp) @staticmethod def compile_qrc(qrc_file, py_file): check_call(['pyrcc4', qrc_file, '-o', py_file]) def get_compiler(self, source_file): name = 'compile_' + source_file.rsplit(os.extsep, 1)[-1] return getattr(self, name, None) @staticmethod def get_module_name(src_filename): name, ext = os.path.splitext(src_filename) return {'.qrc': '%s_rc', '.ui': '%s_ui'}.get(ext, '%s') % name class build_curses(_build_py): description = "build every curses related resource" def finalize_options(self): _build_py.finalize_options(self) self.packages = ['hgviewlib.curses'] class build_doc(_build): description = "build the documentation" def initialize_options (self): self.build_dir = None def finalize_options (self): self.set_undefined_options('build', ('build_doc', 'build_dir')) def run(self): # be sure to compile man page self.mkpath(self.build_dir) if sys.platform.startswith('freebsd'): make_cmd = 'gmake' else: make_cmd = 'make' try: check_call([make_cmd, '-C', self.build_dir, '-f', '../../doc/Makefile', 'VPATH=../../doc']) except: if not py2exe: # does not make sense (either because of windows vs toolchain # or we don't need the doc in the installer) print ('we cannot build the doc,' ' you may want to use --no-doc') raise class build_fullhgext(_build): """XXX ugly hack to include hgext in standalone hgview.exe""" description = "[DO NOT USE] install full mercurial's hgext package (for internal hgview purpose)" def run(self): import hgext shutil.copytree(dirname(hgext.__file__), join(self.build_lib, 'hgext')) class build(_build): user_options = [ ('build-doc=', None, "build directory for documentation"), ('no-qt', None, 'do not build qt resources'), ('no-curses', None, 'do not build curses resources'), ('no-doc', None, 'do not build the documentation'), ] + _build.user_options boolean_options = [ 'no-qt', 'no-curses', 'no-doc' ] + _build.boolean_options def initialize_options (self): _build.initialize_options(self) self.build_doc = None self.no_qt = False self.no_curses = False self.no_doc = False def finalize_options(self): _build.finalize_options(self) for attr in ('with_qt', 'with_curses', 'with_doc'): setdefaultattr(self.distribution, attr, True) if self.build_doc is None: self.build_doc = os.path.join(self.build_base, 'doc') self.distribution.with_qt &= not self.no_qt self.distribution.with_curses &= not self.no_curses self.distribution.with_doc &= not self.no_doc def has_qt(self): return self.distribution.with_qt def has_curses(self): return self.distribution.with_curses def has_doc(self): return self.distribution.with_doc def has_fullhgext(self): """XXX ugly hack to include hgext in standalone hgview.exe""" return py2exe is not None # ugly hack to include every hgext modules # 'sub_commands': a list of commands this command might have to run to # get its work done. See cmd.py for more info. sub_commands = [ ('build_qt', has_qt), ('build_curses', has_curses), ('build_doc', has_doc), ('build_fullhgext', has_fullhgext), ] + _build.sub_commands class install_qt(install_lib): description = "install the qt interface resources" def run(self): if not self.skip_build: self.run_command('build_qt') self.distribution.packages.append('hgviewlib.qt4') install_lib.run(self) class install_curses(install_lib): description = "install the curses interface resources" def run(self): self.distribution.packages.append('hgviewlib.curses') install_lib.run(self) class install_doc(_install_data): description = "install the documentation" def initialize_options (self): _install_data.initialize_options(self) self.install_dir = None self.build_dir = None def finalize_options (self): _install_data.finalize_options(self) self.set_undefined_options('build', ('build_doc', 'build_dir')) self.set_undefined_options('install', ('install_base', 'install_dir')) def run(self): check_call(['make', '-C', self.build_dir, '-f', '../../doc/Makefile', 'VPATH=../../doc', 'install', 'PREFIX=%s' % self.install_dir]) class install(_install): user_options = [ ('no-qt', None, "do not install qt library part"), ('no-curses', None, "do not install curses library part"), ('no-doc', None, "do not install the documentation"), ] + _install.user_options boolean_options = [ 'no-qt', 'no-curses', 'no-doc' ] + _install.boolean_options def initialize_options(self): self.install_doc = None self.no_qt = False self.no_curses = False self.no_doc = False _install.initialize_options(self) def finalize_options(self): _install.finalize_options(self) for attr in ('with_qt', 'with_curses', 'with_doc'): setdefaultattr(self.distribution, attr, True) self.distribution.with_qt &= not self.no_qt self.distribution.with_curses &= not self.no_curses self.distribution.with_doc &= not self.no_doc def has_qt(self): return self.distribution.with_qt def has_curses(self): return self.distribution.with_curses def has_doc(self): return self.distribution.with_doc # 'sub_commands': a list of commands this command might have to run to # get its work done. See cmd.py for more info. sub_commands = [ ('install_qt', has_qt), ('install_curses', has_curses), ('install_doc', has_doc), ] + _install.sub_commands # innosetup monkeypatching if innosetup: # let's help a bit innosetup.py .... long_description = description # innosetup fails with generated multiline long description import codecs codecs.BOM_UTF8 = '' # Ugly hack to correct the BOM erroneously inserted by # innosetup in the generated .iss file def main(): """setup entry point""" # to generate qct MSI installer, you run python setup.py bdist_msi #from setuptools import setup extrargs = {} if py2exe and innosetup: import PyQt4 extra_include = [ 'sip', 'PyQt4', 'PyQt4.QtCore', 'PyQt4.QtGui', 'PyQt4.QtSvg', 'PyQt4.QtXml', 'hgviewlib.qt4.hgqv_ui', 'hgviewlib.qt4.helpviewer_ui', 'hgviewlib.qt4.manifestviewer_ui', 'hgviewlib.qt4.fileviewer_ui', 'hgext.hgview', ] # XXX ugly hack to include hgext in standalone hgview.exe import hgext hgextpath = dirname(hgext.__file__) import glob for f in glob.glob(join(hgextpath, '*.py*')) + glob.glob(join(hgextpath, '*/*.py*')): tmp_f = os.path.splitext(os.path.relpath(f, hgextpath))[0] parts = [i for i in tmp_f.split(os.sep) if i.strip() and i != '__init__' ] m = '.'.join(['hgext']+parts) extra_include.append(m) # end of ugly hack fmtpath = join(dirname(PyQt4.__file__), 'plugins', 'imageformats') global data_files data_files += [('imageformats', [join(fmtpath, 'qsvg4.dll')])] extrargs = dict(windows=[dict(script='bin/hgview_py2exe.py', dest_base='hgview')], options=dict( py2exe=dict( includes=extra_include, excludes=['PyQt4.uic.port_v3'], packages=['hgext', 'email'], ), innosetup=dict( regist_startup=True, # force MinVersion to a valid value ... inno_script= innosetup.DEFAULT_ISS + '[Setup]\nMinVersion=5.0\n', ) ) ) return setup(name=distname, version=version, license=license, description=description, long_description=long_description, author=author, author_email=author_email, url=web, scripts=ensure_scripts(['bin/hgview']), package_dir={modname : modname}, packages=['hgviewlib', 'hgext', 'hgviewlib.hgpatches'], data_files=data_files, ext_modules=ext_modules, cmdclass={'build_qt': build_qt, 'build_curses': build_curses, 'build_doc': build_doc, 'build_fullhgext' : build_fullhgext, 'build' : build, 'install_qt': install_qt, 'install_curses': install_curses, 'install_doc': install_doc, 'install':install, }, **extrargs ) if __name__ == '__main__' : main() hgview-1.9.0/bin/0000775000015700001640000000000012607506603014357 5ustar narvalnarval00000000000000hgview-1.9.0/bin/hgview0000755000015700001640000000210012607505500015560 0ustar narvalnarval00000000000000#!/usr/bin/env python # hgview: visual mercurial graphlog browser in PyQt4 # # Copyright 2008-2010 Logilab # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. """ Hg repository log browser. This may be used as a standalone application or as a hg extension. See README file included. """ import sys from os import readlink, lstat from os.path import join, dirname, abspath, pardir, exists from imp import load_package import stat execpath = abspath(__file__) # resolve symbolic links statinfo = lstat(execpath) if stat.S_ISLNK(statinfo.st_mode): execpath = join(dirname(execpath), readlink(execpath)) execpath = abspath(execpath) # if standalone, import manually setuppath = join(dirname(dirname(execpath)), 'setup.py') if exists(setuppath): # standalone if setup.py found in src dir hgviewlibpath = join(dirname(dirname(execpath)), 'hgviewlib') hgviewlibpath = abspath(hgviewlibpath) load_package('hgviewlib', hgviewlibpath) from hgviewlib.application import main main() hgview-1.9.0/bin/hgview.bat0000644000015700001640000000232012607505500016326 0ustar narvalnarval00000000000000@echo off rem = """-*-Python-*- script rem -------------------- DOS section -------------------- rem You could set PYTHONPATH python -x %~f0 %* goto exit """ # -------------------- Python section -------------------- from PyQt4 import QtCore, QtGui import os import sys import os.path as pos if getattr(sys, 'frozen', None) == "windows_exe": # Standalone version of hgview built with py2exe use its own version # of Mercurial. Using configuration from the global Mercurial.ini will be # ill-advised as the installed version of Mercurial itself may be # different than the one we ship. # # this will be found next to Mercurial.ini path = pos.join(os.path.expanduser('~'), 'hgview.ini') os.environ['HGRCPATH'] = path try: import hgviewlib except ImportError: import stat execpath = pos.abspath(__file__) # resolve symbolic links statinfo = os.lstat(execpath) if stat.S_ISLNK(statinfo.st_mode): execpath = pos.abspath(pos.join(pos.dirname(execpath), os.readlink(execpath))) sys.path.append(pos.abspath(pos.join(pos.dirname(execpath), ".."))) from hgviewlib.application import main main() DosExitLabel = """ :exit rem """