qct/.hg_archival.txt0000644000000000000000000000013611146115774015104 0ustar00usergroup00000000000000repo: 364436139f3626c2647b219539c61f1d00fcf7ce node: c7b4a0789196656f8117676f646507971e0c3e77 qct/.hgignore0000644000000000000000000000033411146115774013621 0ustar00usergroup00000000000000syntax: glob win32\release/*.exe qctlib/ui_preferences.py qctlib/ui_dialog.py *.pyc *.orig .*.swp qct-*.tar.gz build/ qct.egg-info/ dist/ doc/qct.1 doc/qct.1.html .DS_Store syntax: glob qctlib/ui_select.py qct/.hgtags0000644000000000000000000000160411146115774013275 0ustar00usergroup00000000000000c88eaabe985175edd0b4cf39578d0de590ddc07e 0.1 2f71b6c9eab4f62e0a8dc1692ec654f1655985b1 0.2 ff8ec774376bdd661e3f6068aa925f7ec670f91c 0.3 295b63efe66d6444ed6cdb7c59db2549c407b95c 0.4 74135954b4cc9cf1583654816088e6a01886e34d 0.5 99208fe6f0fb09b1601296415680827e4cd07d91 0.6 58e85617c0ac3d2ce22842d17b10ccaef0c52012 0.6 5c4bef03f608e1e98bcfefea6bd8c0595e351db7 0.7 2bb227dcb6eb291b3e4bb2de5a4541b0fe089190 0.8 2ec740420d2969ed3a582c97963d902362b52c81 0.9 27715100e6f76f135319e183dd6ed6f5992a6072 1.0 5c4da8ab3f292dd7f13e7140563fa5655f14d95c 1.0 cd3bdb7e58ae770770b72862ef8f2522bf95b4ee 1.0 e4d854eea720d463186206559462f1316f13e3d3 1.2 e700a5008b2a9a34a06cb2e6bcec1a4ccfa17a40 1.3 4ca45fadd2f8fa750b338b38da49c648dae682b3 1.3 ad5eb4936459bca22ae06776c9245ccfabe9b6a4 1.4 93f3617ebb2872e13aee80cd9a0a4de23647ab79 1.5 8d1adaef15669b98d39ab83d186c3543e7a8142e 1.6 cf882dee9b9721044ccf9a48d5f279c286f1bcbe 1.7 qct/COPYING0000644000000000000000000004311011146115774013050 0ustar00usergroup00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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. qct/Changelog0000644000000000000000000000631311146115774013633 0ustar00usergroup00000000000000Run 'hg log --style=changelog' to get a per-commit listing Version 1.0: Expect signed 32 bit return codes from Windows Minor fix to allow Qct to run on PyQt4.0 Added a clear-filter button and shortcut Ctrl-F Added a configurabe for a visual history browser Version 0.9: Support for Subversion repositories Support for Cogito (git) repositories Support for Monotone repositories Support for CVS repositories Support -I/-X options in the Mercurial extension Add sign-off message to preferences dialog Sort file list by [filename | extension | status] File filtering (simple string matching) Added --version and --help command line arguments Version 0.8: Change selection/cherry-picking (uses external two-way merge) Context menus: -- add ignore masks for unknown files -- select copy sources for unknown and ignored files -- delete unknown and ignored files -- find rename/move targets for missing files -- revert, visual diff, external editor for all revisioned commitable files Preferences dialog for specifying external tools Progress bar for the repository scan Simple repository auto-detection for standalone qct Revert button removed, replaced with a Cancel button Support for automatic sign-off messages in Hg back-end Directory/module layout changes (do a fresh install) Version 0.7: Support for Mercurial Queues Cache scheme for hg back-end Make most recent commit log messages available for re-use Enable multi-selection in the file-list Improved support for symbolic links in Mercurial back-end Added a man-page Worked around a Qt problem (multiple press events from one click) Worked around more Windows path problems setup.py will build UI file iff it does not exist Version 0.6: Moved qct logic unto qctlib/ package, for cleaner installation Added check boxes to file list to indicate selected state Removed userName edit field Added movable splitter between file list and diff browser Keep window geometry (and splitter) persistent in ~/.config/vcs/qct.conf Added CTRL-U shortcut to unselect all files Added CTRL-[] shortcuts to page the diff browser up and down Added '(Un)Select All' push button Hard-coded Sans Serif font for diff browser Revert from QDialog to QWidget, for WM reasons Added support for log message templates $(repo-root)/.commit.message OSX tuned optional UI layout (see INSTALL) Version 0.5: Added an experimental perforce back-end More layout improvements Bug fixes to CTRL-N behavior Fixes for PyQt 4.0 Version 0.4: Added syntax highlighting to diffBrowser window (thanks to hgct) Layout improvements, new CTRL-N shortcut to cycle through diffs Add an automated distribution Makefile rule Added a back-end for bazaar, including a plugin Keep selected list persistent through refresh events Version 0.3: Added a 'Revert Selected' button, with warning dialog message Added 'Ctrl+O' shortcut for commit, 'Esc' shortcut for abort Stubbed in a git back-end Cleaned up VCS back-end interface Separated GUI logic from stand-alone app Mercurial extension now spawns a GUI without forking another process Version 0.2: Added mercurial extension Improved layout and work-flow Version 0.1: Initial integration with Mercurial Commit dialog window works qct/INSTALL0000644000000000000000000000720211146115774013050 0ustar00usergroup00000000000000======= Installing the stand-alone application (Unix/Linux) ======= The simplest approach is to do this: qct% make install This will try to build qctlib/ui_dialog.py, which is only necessary if you pulled sources directly out of revision control. This file is supplied with the release tarballs. Note that building ui_dialog.py requires Qt >= 4.2 and PyQt >= 4.1. The install rule then runs: python setup.py install --home=~ This will copy the qct standalone application into ~/bin and the python library files into ~/lib/python/qctlib. You must ensure ~/lib/python is in your PYTHONPATH for qct to work correctly. To test the installation you can try running 'qct' from inside any supported repository. /home/user/hgRepo% qct --hg ======= Enabling Change Selection ======= To enable change selection (cherry picking which changes you wish to commit), you must have a two-way merge program installed and registered in the preferences dialog. Suggested two-way merge programs: kompare, gpyfm, meld, or kdiff3 (in that order). Here are the commandlines I suggest for each tool (to enter into the 2-way diff configurable) kompare gpyfm %o %o %m meld kdiff3 %m %o -o %o -L1 modified -L2 original ======= Installing site-wide (Unix/Linux) ======= First you must be root, and then run: qct% make site-install You can download VCS plugins from these links: http://qct.sourceforge.net/hg/qct/raw-file/tip/hgext/qct.py http://qct.sourceforge.net/hg/qct/raw-file/tip/plugins/qctBzrPlugin.py ====== OSX Installation ====== Gain permission to install software, make sure you have a functioning installation of Python2.5, Qt4, and PyQt4 for OSX, then run: qct% make site-install Note that some people have reported problems with pyuic4 on OSX. If you are having trouble starting Qct, try using the qctlib/ui_*.py files out of a release-tarball. In the future, I would like to package binary applications for Qct on OSX just like I do for Windows. ====== Windows Installation (Python Source) ====== Note: Windows has a known limitation of 32Kbytes for it's command line. Since Qct operates directly with the VCS command line tools, it is possible to run into this limit. To install qct from sources, you should follow this approach: 1) If you do not already have it, download and build qt4 (>=4.2) 3) If you do not already have it, download and install python (>=2.5) 3) If you do not already have it, download and install PyQt4 (>=4.1) 4) Make sure $(QT_DIR)/bin is in your path 5) Download qct, cd qct-*, make 6) Test qct by running: python qct 7) If qct cannot find your underlying VCS tool, you may need to create a batch file somewhere in your path which calls your VCS appropriately. Here's an example hg.bat for Mercurial: @c:\Python25\python.exe c:\Python25\Scripts\hg %* 8) now run: python setup.py install 9) Install the Mercurial or bazaar plugin, as needed 10) If you don't intend to use qct as a plugin, you'll need to use the included batch file `win32/qct.bat` to invoke the standalone application. By default, the batch file is setup to use the perforce back-end. If you use a different VCS you should edit the batch file before copying it into your path. Don't forget to read the README file associated with your revision control back-end. ======== Windows Installation (Binary Package) ======== Download and install the latest binary release package http://qct.sourceforge.net/qct-N.N-win32.exe Don't forget to read the README file associated with your revision control back-end. To use qct with mercurial on windows, it's recommended you use the Mercurial+Qct "Batteries Included" installer available from http://qct.sourceforge.net qct/Makefile0000644000000000000000000000201311146115774013452 0ustar00usergroup00000000000000# # Distribution logic # # Files not revisioned which are distributed UI_FILES := qctlib/ui_dialog.py qctlib/ui_preferences.py UI_FILES += qctlib/ui_select.py EXTRAS := $(UI_FILES) doc/qct.1 doc/qct.1.html # Revisioned files which are not distributed PRUNE := .hgtags .hgignore VER := $(shell python setup.py --version) DIST_NAME := qct-$(VER) PRUNE_LIST := $(foreach file, $(PRUNE), $(DIST_NAME)/$(file)) all: $(UI_FILES) dist: $(EXTRAS) hg archive --type=files $(DIST_NAME) for i in $(EXTRAS) ; do cp $$i $(DIST_NAME)/$$i ; done $(RM) $(PRUNE_LIST) tar czf $(DIST_NAME).tar.gz $(DIST_NAME)/ $(RM) -r $(DIST_NAME)/ ui_%.py : %.ui pyuic4 $< > $@ doc/qct.1: $(MAKE) -C doc .PHONY: install site-install mac-install install: $(UI_FILES) python setup.py install --home=~ site-install: $(UI_FILES) python setup.py install --force egg: $(UI_FILES) python setup.py bdist_egg clean: $(RM) $(UI_FILES) $(RM) qctlib/*.pyc qctlib/vcs/*.pyc $(RM) -r build/ dist/ qct.egg-info/ $(RM) qct-*.tar.gz $(MAKE) -C doc clean qct/README0000644000000000000000000000235111146115774012677 0ustar00usergroup00000000000000QCT - Qt/PyQt based commit tool Author: Steve Borho Downloads: http://qct.sourceforge.net/qct-version-win32.exe - Windows self-extracting zip http://qct.sourceforge.net/qct-version.tar.gz - Source code http://qct.sourceforge.net/qct-version-py2.5.egg - Python Egg http://qct.sourceforge.net/hg/qct - Mercurial Repository Primary goals: 1) Cross-Platform (Linux, Windows-Native, MacOS, cygwin) 2) Be VCS agnostic. 3) Good keyboard navigation, keep the typical work-flow simple Keyboard Shortcuts: CTRL-O - Commit selected files CTRL-R - Refresh file list CTRL-N - View diffs of next file in list CTRL-[] - Page up/down through file diffs CTRL-U - Unselect all files CTRL-F - Clear file filter text ESC - Abort and exit The qct standalone application will attempt to auto-detect the repository you are running from, but will also accept a command line argument specifying the VCS back-end you wish to use. See the man page for more usage details and consult the README file for the version control system you wish to use. Known Issues: Windows has a command line limit of 32KBytes. If you break this limit you get to keep both halves :) http://blogs.msdn.com/oldnewthing/archive/2003/12/10/56028.aspx qct/README.bazaar0000644000000000000000000000317211146115774014140 0ustar00usergroup00000000000000====== Using Qct with Bazaar ====== Qct can run as a bazaar plugin. Just copy plugins/qctBzrPlugin.py to your ~/.bazaar/plugins directory. You can test this installation by running: % bzr help qct If you use the selected change feature (by registering a two-way merge application), it is suggested that you add .qct/ to your ignore mask to prevent the backed up working directory files from showing up in the file list. To use a commit message template, place the template message here: $(bzr-root)/.commit.template Bazaar does support an experimental plugin for calling external (GUI) diff browsers. You can enable this plugin and then register your diff command with Qct in the preferences dialog. Qct will then offer you a visual diff context menu option for all modified files in your repository. Look for difftools on this page: http://bazaar-vcs.org/BzrPlugins Note that if you chose not to use the Bazaar plugin, qct can still work with Bazaar repositories. If you start the standalone application `qct` inside the repository root (where .bzr/ resides), qct will autodetect the Bazaar back-end. If you start it elsewhere in the repository directory tree, you will need to supply a command line argument to specify Bazaar: qct --bzr In theory, Qct should work with Bazaar on Windows, but this has never been tested. See README.mercurial for details about how batch files are sometimes necessary when Qct tries to call external python applications. PS: The Bazaar back-end and plugin are not as well tested as I would like. If you find a bug or find support for a particular bzr feature not to your liking, please send me an e-mail. qct/README.cvs0000644000000000000000000000154611146115774013476 0ustar00usergroup00000000000000====== Using Qct with CVS ====== Qct can auto-detect CVS repositories, so you can run the standalone application from inside any CVS repo without any setup. However, qct does accept a '--cvs' command line option for completeness. Usage Notes: o Assumes you have a working cvs command line tool o Assumes you have CVSROOT setup appropriately o Assumes there is no passphrase required for rsh/ssh access o Does not support listing ignored files (cvs seems broken in this regard) For a commit message template, place the template message into ~/.commit.template PS: The CVS back-end is not as well tested as I would like. If you find a bug or find support for a particular cvs feature not to your liking, please send me an e-mail. In particular, I would like feedback on how well Qct works with CVS on Windows, and how well it deals with merge conflicts. qct/README.git0000644000000000000000000000136111146115774013461 0ustar00usergroup00000000000000====== Using Qct with Git ====== Git repositories are only supported via the Cogito front-end scripts, which must be installed in order for Qct to function properly. For a commit message template, place the template file in your repository root: $(git-root)/.commit.template If you start qct inside the repository root (where .git/ resides), qct will autodetect the Cogito back-end. If you start it elsewhere in the repository directory tree, you will need to supply a command line argument to specify Cogito: qct --cg PS: The Cogito back-end is not as well tested as I would like. If you find a bug or find support for a particular cg feature not to your liking, please send me an e-mail. Note: For a Git-Native commit tool, give GCT a try. qct/README.mercurial0000644000000000000000000000772211146115774014670 0ustar00usergroup00000000000000========================================= ====== Using Qct with Mercurial ====== ========================================= ====== Configuring qct.py extension ====== You will want to install the hgext/qct.py extension for Mercurial for both performance and functionality reasons. It allows you to launch qct as a Mercurial command, e.g.: 'hg qct' or 'hg commit-tool'. To use the qct.py extension, you need to specify in your ~/.hgrc file where Mercurial can find qct.py. You can either copy qct.py into ~/lib/python/hgext, along with the other Mercurial extensions (if you installed Mercurial globally, then hgext/ will be in your python site-packages) and enable it in your ~/.hgrc via: [extensions] hgext.qct = or simply specify the full path to it via: [extensions] qct = /path/to/qct.py For more extension info, read these: http://www.selenic.com/mercurial/wiki/index.cgi/QctExtension http://www.selenic.com/mercurial/wiki/index.cgi/ExtensionHowto You can test your extension installation by running: % hg help qct ====== Configuring Visual Diffs ====== To enable visual diffs inside the context menus of modified files, you must enable the extdiff extension and then define a vdiff command, for example: [extensions] hgext.extdiff = [extdiff] cmd.vdiff = kdiff3 You then must specify 'hg vdiff' in the 'External Diff Tool' section of the Qct preferences dialog. ====== Sign-Off Message ====== To enable an automatic sign-off message to be appended to all of your commits, you can add lines like these to your ~/.hgrc file (to take effect globally) or in a repository's $(root)/.hg/hgrc file (to override locally): [qct] signoff = Sign-Off: Steve Borho You can also specify a sign-off messsage in the Qct preferences dialog. ====== Commit Message Template ====== For a commit message template, place the template file in your repository root: $(hg-root)/.commit.template ====== Running Qct with Mercurial ====== Standalone Qct normally tries to operate out of current directory, but the extension layer will enforce the Mercurial standard behavior. For instance: hg qct - will run in global (repo-wide) context hg qct . - will run in local context The Mercurial extension respects the -I and -X command line arguments for filtering the parts of the repository you are interested in. It also respects the --user command line option for specifying a username for this single commit (overriding ui.username). See 'hg help qct' for more details. Note that if you chose not to use the Mercurial extension, Qct can still work with Mercurial repositories. If you start the standalone application `qct` inside the repository root (where .hg/ resides), qct will autodetect the Mercurial back-end. If you start it elsewhere in the repository directory tree, you will need to supply a command line argument to specify Mercurial vcs: qct --hg ====== Using Change Selection ====== If you use the change selection feature, you will want to make sure *.orig is in your .hgignore mask (which is usually already the case since Mercurial will create .orig files when reverting). If you do not do this, there's a good chance you'll see .qct/ backup files in the file list (listed as unknown) if you refresh the list after selecting changes. =========== Windows Qct+Mercurial Notes ============ (See INSTALL file for details on installing Qct on Windows) If Qct cannot find the hg executable (complains of not finding 'hg root'), you probably need to create a batch file somewhere in your path which calls hg appropriately. Here's an example hg.bat: @c:\Python25\python.exe c:\Python25\Scripts\hg %* If you have installed a binary version of qct.exe, and plan to use the qct.py extension, you will not have to add qct.exe to your path. Instead, you just need to follow the instructions above for installing the qct.py extension (available here): http://bitbucket.org/sborho/qct/raw/tip/hgext/qct.py Then add two more lines to your Mercurial.ini file like this: [qct] path = "C:\Path\to\Qct\qct.exe" qct/README.monotone0000644000000000000000000000207211146115774014534 0ustar00usergroup00000000000000====== Using Qct with Monotone ====== You must specify a get_passphrase() in your ~/.monotone/monotonerc in order for Qct to commit files. Note that this has obvious security ramifications, but dem's the breaks. function get_passphrase(keypair_id) return "myphrase" end If you use the selected change feature, it is suggested that you add .qct/ to your .mtn-ignore mask to prevent the backed up working directory files from showing up in the file list. Qct's support for adding ignore masks for unknown files only works properly when Qct is run out of the repository root (where .mtn-ignore is). Patches welcome for removing this shortcoming. If you start qct inside the repository root (where .mtn/ resides), qct will autodetect the Monotone back-end. If you start it elsewhere in the repository directory tree, you will need to supply a command line argument to specify Monotone: qct --mtn PS: The Monotone back-end is not as well tested as I would like. If you find a bug or find support for a particular mtn feature not to your liking, please send me an e-mail. qct/README.perforce0000644000000000000000000000242511146115774014505 0ustar00usergroup00000000000000**** The perforce back-end is currently broken **** ====== Using Qct with Perforce ====== Perforce does not support extensions, and the Qct standalone application cannot auto-detect a perforce repository, so a shell alias or batch file can be useful: alias p4qct='qct --p4' For a commit message template, place the template message into a file and then set P4_LOG_TEMPLATE environment variable to reference that file. Usage Notes: o Assumes P4CLIENT, P4PORT, P4USER are properly set o Assumes a valid login to the perforce server o Will execute out of current directory (cannot find client root) o Will always use the default changelist Perforce on Windows: o You must have the Windows command line tools installed (p4.exe) o You must override P4DIFF in your environment to run a command line diff.exe, such as one that comes with cygwin or MinGW. o The included win32/qct.bat file can be used to call qct with the necessary --p4 command line option. Note: In my experience, P4Diff does not work properly as a two-way merge application, but it should be functional for visual diffs. PS: The Perforce back-end is not as well tested as I would like. If you find a bug or find support for a particular p4 feature not to your liking, please send me an e-mail. qct/README.subversion0000644000000000000000000000070011146115774015071 0ustar00usergroup00000000000000====== Using Qct with Subversion ====== Usage Notes: * Assumes you have a working svn command line tool * Assumes there is no passphrase required for rsh/ssh access For a commit message template, place the template file in your home directory: ~/.commit.template PS: The Subversion back-end is not as well tested as I would like. If you find a bug or find support for a particular svn feature not to your liking, please send me an e-mail. qct/TODO0000644000000000000000000000022411146115774012504 0ustar00usergroup00000000000000TODO Tasks: Handle encoding issues Get more testing of Cogito, Subversion, CVS, and Bazaar back-ends File-status check-boxes in preferences dialog qct/doc/Makefile0000644000000000000000000000107211146115774014223 0ustar00usergroup00000000000000SOURCES=$(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 $*.txt %.html: %.txt asciidoc -b html4 $*.txt || asciidoc -b html $*.txt install: man for i in $(MAN) ; do \ subdir=`echo $$i | sed -n 's/..*\.\([0-9]\)$$/man\1/p'` ; \ mkdir -p $(MANDIR)/$$subdir ; \ $(INSTALL) $$i $(MANDIR)/$$subdir ; \ done clean: $(RM) $(MAN) $(MAN:%=%.xml) $(MAN:%=%.html) qct/doc/README0000644000000000000000000000101711146115774013442 0ustar00usergroup00000000000000Qct's documentation is currently kept in ASCIIDOC format, which is a simple plain text format that's easy to read and edit. It's also convertible to a variety of other formats including standard UNIX man page format and HTML. To do this, you'll need to install ASCIIDOC: http://www.methods.co.nz/asciidoc/ To generate the man page: asciidoc -d manpage -b docbook qct.1.txt xmlto man qct.1.xml To display: groff -mandoc -Tascii qct.1 | more To create the html page (without stylesheets): asciidoc -b html qct.1.txt qct/doc/qct.1.txt0000644000000000000000000000765611146115774014270 0ustar00usergroup00000000000000qct(1) ===== Steve Borho NAME ---- qct - Qt Commit Tool SYNOPSIS -------- 'qct' [VCS] DESCRIPTION ----------- The qct(1) command provides a common GUI commit dialog for many revision control systems across many platforms, including Linux/UNIX, MacOSX, and Microsoft Windows. VERSION CONTROL SYSTEMS ----------------------- Mercurial:: [--hg|-h] Qct supports both the simple repository model and the Mercurial Queue patch maintenance model. When MQ patches are applied, qct will present a patch refresh user interface. Consult README.mercurial for more details. Bazaar:: [--bzr|-b] Bazaar support is complete, but could use some polishing. Qct can run as a bazaar plugin. Consult README.bazaar for more details. Perforce:: [--p4|-4] Perforce support is complete, if somewhat restrictive. On Windows, you must override P4DIFF with a command line diff tool. Consult README.perforce for more details. CVS:: [--cvs|-c] CVS support is feature complete, but not well tested. Consult README.cvs for more details. Monotone:: [--mtn|-m] Monotone support is feature complete, but not well tested. Consult README.monotone for more details. Subversion:: [--svn|-s] Subversion support is not very well tested at this time, so consider it alpha quality. Consult README.subversion for more details. Git:: [--cg] Git repositories are only supported via the Cogito front-end interface, which must be installed in order for Qct to work properly. Consult README.git for more details. VCS INTEGRATION --------------- Qct can run as a plugin inside both Mercurial and Bazaar. Please consult the packaged INSTALL file and each plugin's built-in help for more information. SIGN OFF MESSAGES ----------------- If you require a sign-off (or other) message to be appended to all of your commit messages, you can specify this message in the Qct preferences dialog. The sign-off message will not show up in the commit message window, but will be automatically appended to your message when passed to the VCS for commit. CHANGE SELECTION ---------------- Qct will allow you to select individual changes made to revisioned files, temporarily storing the remaining changes under a .qct/ directory until the commit has been completed. To enable this feature, you must configure a two-way merge application in the Qct preferences dialog. Kompare, meld, and kdiff3 are all known to work correctly in this mode. Any merge application which takes two file-names on the command line and allows them to be merged together can be used. EXTERNAL EDITOR --------------- You can register an external editor with the Qct preferences dialog. Your editor will be presented as a context-menu option for all non-deleted commitable files in the file list. EXTERNAL DIFF ------------- If your VCS supports external diff tools, you may register one of these with the Qct preferences dialog. Your diff tool will be offered in the context menu of all modified files in your file list. The diff tool will be provided with the list of selected files so it must be capable of retrieving the file diffs itself, typically by getting them from your revision control system. FILES ----- ~/.config/vcs/qct:: This file contains persistent data stored by Qct between invocations. It is not meant to be user modified. BUGS ---- Probably lots, please send them to be via e-mail when you find them. Patches (or mercurial bundles) are always welcome. Windows has a known limitation of 32Kbytes for it's command line. Since Qct operates directly with the VCS command line tools, it is possible to run into this limit. AUTHOR ------ Written by Steve Borho RESOURCES --------- http://qct.sourceforge.net/[Web Page] http://qct.sourceforge.net/hg/qct[Source code repository] COPYING ------- Copyright \(C) 2006 Steve Borho Free use of this software is granted under the terms of the GNU General Public License (GPL) qct/hgext/qct.py0000644000000000000000000000631411146115774014302 0ustar00usergroup00000000000000# Qct commit tool extension for Mercurial # # Copyright 2006 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. '''Qt commit tool''' from mercurial import hg, commands, dispatch import os # 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 launch_qct(ui, repo, *extras, **opts): """start qct commit tool If '.' is given as an argument the tool will operate out of the current directory, else it will operate repository-wide. This command will open a window from which you can browse all of the commitable changes you have made to your working directory. You can then enter a log message and commit your changes to the repository. You can remove files from the commit list by de-selecting them in the file list. Keyboard Shortcuts: CTRL-O - Commit selected files CTRL-R - Refresh file list CTRL-N - View diffs of next file in list CTRL-[] - Page up/down through file diffs CTRL-U - Unselect all files CTRL-F - Clear file filter text ESC - Abort and exit """ rundir = repo.root if '.' in extras: rundir = '.' os.chdir(rundir) # If this user has a username validation hook enabled, # it could conflict with Qct 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 Qct # allocates the app try: from hgconf.uname import hook hook(ui, repo) except ImportError: pass try: from PyQt4 import QtGui from qctlib.gui_logic import CommitTool from qctlib.vcs.hg import qctVcsHg except ImportError: # If we're unable to import Qt4 and qctlib, try to # run the application directly # You can specificy it's location in ~/.hgrc via # [qct] # path= try: udata = " -u %s" % opts['user'][-1] except: udata = '' cmd = ui.config("qct", "path", "qct") + udata + " --hg" os.system(cmd) else: import sys vcs = qctVcsHg() if hasattr(commands, "dispatch"): # 0.9.4 and below if vcs.initRepo(None, commands) != 0: sys.exit() else: if vcs.initRepo(None, dispatch) != 0: sys.exit() # Pass along -I/-X and --user options to Mercurial back-end vcs.pluginOptions(opts) app = QtGui.QApplication([]) dialog = CommitTool(vcs) dialog.show() app.exec_() cmdtable = { "^qct|commit-tool": (launch_qct, [('I', 'include', [], 'include names matching the given patterns'), ('X', 'exclude', [], 'exclude names matching the given patterns'), ('u', 'user', [], 'record user as committer')], "hg qct [options] [.]") } qct/plugins/qctBzrPlugin.py0000644000000000000000000000421711146115774016501 0ustar00usergroup00000000000000# Qct commit tool plugin for bazaar (bzr) # # Copyright 2006 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. # # To install, copy this file into ~/.bazaar/plugins from bzrlib.commands import Command, register_command class cmd_qct(Command): """Qt-based GUI Commit Tool This command will open a window from which you can browse all of the pending changes you have made to your branch. You can then type in a commit log message and commit your changes to the repository. You can remove files from the commit list by de-selecting them in the file list. Keyboard Shortcuts: CTRL-O - Commit selected files CTRL-R - Refresh file list CTRL-N - View diffs of next file in list CTRL-[] - Page up/down through file diffs CTRL-U - Unselect all files CTRL-F - Clear file filter text ESC - Abort and exit """ def run(self): from bzrlib.branch import Branch from bzrlib.errors import NoWorkingTree from bzrlib.workingtree import WorkingTree from bzrlib import urlutils import os from PyQt4 import QtCore, QtGui from qctlib.gui_logic import CommitTool from qctlib.vcs.bzr import qctVcsBzr def local_path(path): if path.startswith("file://"): return urlutils.local_path_from_url(path) else: return urlutils.unescape(path) try: branch = WorkingTree.open_containing(u'.')[0].branch except NoWorkingTree: branch = Branch.open_containing(u'.')[0] branch_root = branch.bzrdir.root_transport.base # print "Branch root at " + branch_root os.chdir(local_path(branch_root)) vcs = qctVcsBzr() if vcs.initRepo(None) != 0: return try: app = QtGui.QApplication([]) dialog = CommitTool(vcs) dialog.show() app.exec_() except SystemExit: pass register_command(cmd_qct) qct/qct0000644000000000000000000001157411146115774012540 0ustar00usergroup00000000000000#!/usr/bin/env python # qct - Commit Tool # # Copyright 2006 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. import sys, os import getopt from PyQt4 import QtGui from qctlib.gui_logic import CommitTool def show_help(): 'Show help' print '''Qct GUI Commit Tool Qct will attempt to auto-detect the repository you are running inside of, but will also accept a command line argument specifying the VCS back-end you intend to use. qct [ --hg | --p4 | --svn | --bzr | --cvs | --mtn | --cg ] qct [ -h | -4 | -s | -b | -c | -m ] qct [ --help | --version ] See the man page for more usage details and consult the INSTALL file for more detailed documentation about each VCS back-end. qct always tries to operate out of current directory, but in some instances it must move context to your repository root. Keyboard Shortcuts: CTRL-O - Commit selected files CTRL-R - Refresh file list CTRL-N - View diffs of next file in list CTRL-[] - Page up/down through file diffs CTRL-U - Unselect all files CTRL-F - Clear file filter text ESC - Abort and exit''' # Standalone command line application if __name__ == "__main__": # parse command line options opts = [] try: opts, args = getopt.getopt(sys.argv[1:], "s4bghcmvu:", ["svn", "p4", "bzr", "hg", "cvs", "mtn", "version", "help", "cg", "gui"]) except getopt.error, msg: pass app = QtGui.QApplication(sys.argv) for o in opts: if o[0] == "--gui": class DirDialog(QtGui.QFileDialog): def __init__(self, parent=None): QtGui.QFileDialog.__init__(self, parent) def show (self): dialog = QtGui.QFileDialog.getExistingDirectory (self, "Open Directory", "", QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontResolveSymlinks) return dialog dirdialog = DirDialog() ret = dirdialog.show() os.chdir (ret) opts.remove (o) break vcs = None initRepoArgs = {} if len(opts) == 0: # default to auto-detect VCS back-end if no matches were found if os.path.exists('.hg/'): print "Auto-detected Mercurial repository" from qctlib.vcs.hg import qctVcsHg vcs = qctVcsHg() elif os.path.exists('.bzr/'): print "Auto-detected Bazaar repository" from qctlib.vcs.bzr import qctVcsBzr vcs = qctVcsBzr() elif os.path.exists('.svn/'): print "Auto-detected Subversion repository" from qctlib.vcs.svn import qctVcsSvn vcs = qctVcsSvn() elif os.path.exists('.git/'): print "Auto-detected Git (Cogito) repository" from qctlib.vcs.cg import qctVcsCg vcs = qctVcsCg() elif os.path.exists('_MTN/'): print "Auto-detected Monotone repository" from qctlib.vcs.mtn import qctVcsMtn vcs = qctVcsMtn() elif os.path.exists('CVS/'): print "Auto-detected CVS repository" from qctlib.vcs.cvs import qctVcsCvs vcs = qctVcsCvs() else: from qctlib.vcs.hg import qctVcsHg vcs = qctVcsHg() # process options for opt, arg in opts: if opt in ('--version', '-v'): from qctlib.version import qct_version print qct_version elif opt == '-u': initRepoArgs['username'] = arg elif opt == '--help': show_help() elif opt == '--cg': from qctlib.vcs.cg import qctVcsCg vcs = qctVcsCg() elif opt in ("-4", "--p4"): from qctlib.vcs.p4 import qctVcsP4 vcs = qctVcsP4() elif opt in ("-h", "--hg"): from qctlib.vcs.hg import qctVcsHg vcs = qctVcsHg() elif opt in ("-b", "--bzr"): from qctlib.vcs.bzr import qctVcsBzr vcs = qctVcsBzr() elif opt in ("-s", "--svn"): from qctlib.vcs.svn import qctVcsSvn vcs = qctVcsSvn() elif opt in ("-c", "--cvs"): from qctlib.vcs.cvs import qctVcsCvs vcs = qctVcsCvs() elif opt in ("-m", "--mtn"): from qctlib.vcs.mtn import qctVcsMtn vcs = qctVcsMtn() if not vcs or vcs.initRepo(sys.argv, **initRepoArgs) != 0: sys.exit() # Now we know it's worth the trouble to open the GUI dialog = CommitTool(vcs) dialog.show() sys.exit(app.exec_()) qct/qct.rc0000644000000000000000000000040211146115774013127 0ustar00usergroup00000000000000# This file is intended to be installed in a system's hgrc.d directory # It enables the qct's mercurial plugin, making the 'hg qct' command # available. This file presumes hgext/qct.py has been installed in # the hgext path [extensions] hgext.qct = qct/qctlib/__init__.py0000644000000000000000000000001611146115774015402 0ustar00usergroup00000000000000# placeholder qct/qctlib/changeselect.py0000755000000000000000000001132611146115774016301 0ustar00usergroup00000000000000#!/usr/bin/env python # Change Selection Dialog # # Copyright 2007 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. from PyQt4.QtCore import * from PyQt4.QtGui import * from qctlib.ui_select import Ui_ChangeDialog from qctlib.utils import formatPatchRichText from qctlib.patches import iter_patched, hunk_from_header import difflib patchColors = { 'std': 'black', 'new': '#009600', 'remove': '#C80000', 'head': '#C800C8'} class ChangeDialog(QDialog): '''QCT commit tool GUI logic''' def __init__(self, changefile, origfile): '''Initialize the dialog window, calculate diff hunks''' QDialog.__init__(self) self.changefile = changefile self.origfile = origfile self.accepted = False self.ui = Ui_ChangeDialog() self.ui.setupUi(self) self.ui.buttonBox.setStandardButtons(QDialogButtonBox.Cancel) settings = QSettings('vcs', 'qct') settings.beginGroup('changetool') if settings.contains('size'): self.resize(settings.value('size').toSize()) self.move(settings.value('pos').toPoint()) settings.endGroup() # Calculate unified diffs, split into hunks self.hunklist = [] hunk = None self.origtext = open(origfile, 'rb').readlines() changetext = open(changefile, 'rb').readlines() for line in difflib.unified_diff(self.origtext, changetext): if line.startswith('@@'): if hunk: self.hunklist.append(hunk) hunk = [line] elif hunk: hunk.append(line) if hunk: self.hunklist.append(hunk) self.curhunk = 0 self.showhunk() def showhunk(self): text = ''.join(self.hunklist[self.curhunk]) h = formatPatchRichText(text, patchColors) self.ui.textEdit.setHtml(h) def on_keepButton_pressed(self): '''User has pushed the keep button''' self.curhunk += 1 if self.curhunk < len(self.hunklist): self.showhunk() else: self.ui.keepButton.setEnabled(False) self.ui.shelveButton.setEnabled(False) self.ui.buttonBox.setStandardButtons( \ QDialogButtonBox.Cancel | QDialogButtonBox.Ok) def on_shelveButton_pressed(self): del self.hunklist[self.curhunk] if self.curhunk < len(self.hunklist): self.showhunk() else: self.ui.keepButton.setEnabled(False) self.ui.shelveButton.setEnabled(False) self.ui.buttonBox.setStandardButtons( \ QDialogButtonBox.Cancel | QDialogButtonBox.Ok) def reject(self): '''User has pushed the cancel button''' self.close() def closeEvent(self, e = None): '''Dialog is closing, save persistent state''' settings = QSettings('vcs', 'qct') settings.beginGroup('changetool') settings.setValue("size", QVariant(self.size())) settings.setValue("pos", QVariant(self.pos())) settings.endGroup() settings.sync() if e is not None: e.accept() def accept(self): '''User has pushed the accept button''' # Make sure at least one hunk was selected if not self.hunklist: self.close() return # Determine lines we expect from iter_patched to replace lasthunk = self.hunklist[-1] lasth = hunk_from_header(lasthunk[0]) # Apply selected hunks to origfile try: try: patchlines = ['--- orig\n', '+++ changed\n'] for p in self.hunklist: patchlines += p replace = [] for line in iter_patched(self.origtext, patchlines): replace.append(line) if replace: self.origtext[0:lasth.orig_pos + lasth.orig_range - 1] = \ replace f = open(self.origfile, 'wb') f.writelines(self.origtext) f.close() self.accepted = True except Exception, e: print str(e) finally: self.close() # Test operation by running directly if __name__ == "__main__": import sys app = QApplication(sys.argv) try: (changefile, origfile) = sys.argv[1:3] except ValueError: print sys.argv[0], 'changed-file orig-file' sys.exit(1) dialog = ChangeDialog(changefile, origfile) dialog.show() app.exec_() if dialog.accepted: sys.exit(0) else: print 'changes not selected' sys.exit(1) qct/qctlib/dialog.ui0000644000000000000000000005323711146115774015104 0ustar00usergroup00000000000000 Steve Borho commitToolDialog 0 0 672 546 0 0 672 0 Commit Tool 6 9 9 9 9 Qt::Vertical 0 0 0 132 16777215 16777215 Commit Message 6 9 9 9 9 0 0 16777215 16777215 Monospace <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Here you enter a log message describing the changes made to the selected files. There are several useful keyboard shortcuts:</p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">CTRL-N - display next file</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">CTRL-[ - page up file changes</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">CTRL-] - page down file changes</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">CTRL-O - commit</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">ESC - abort</p></body></html> true <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Monospace'; font-size:9pt; font-weight:400; font-style:normal;"> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif';"></p></body></html> false 6 0 0 0 0 Most Recent: logHistComboBox Qt::ClickFocus <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">This combo box allows you to select one of your most recent commit messages. The selected message is copied into the commit message text area, overwriting any text that was already there.</p></body></html> QComboBox::InsertAtTop Qt::Horizontal 40 20 0 0 0 0 16777215 16777215 File List 6 9 9 9 9 6 0 0 0 0 <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">This button will attempt to select (check) every file in the file list. If all of the files were already selected, it will unselect them all instead.</p></body></html> (Un)Select All Unsorted Sort By Path/Name Sort By Status Reverse By Status Sort By Extension Filter: filterLineEdit 0 0 Qt::RightArrow Qt::Horizontal 131 24 <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">This button causes the revision control system to re-inspect the state of the repository. This should only be necessary if you have modified the repository since this dialog was opened.</p></body></html> &Refresh List 0 0 16777215 16777215 <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">This is the list of commitable files. Files which are checked will be commited together as a group when the commit button (or CTRL-O) is pressed. </p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Note that the filter does not effect a file's checked status. Files which are checked but not currently displayed are still considered as part of the commit group.</p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">At startup, and after each commit, Qct will automatically preselect all the commitable files which are already revisioned. This is for your convenience, since in most cases you will want to commit these files together as a group.</p></body></html> true QAbstractItemView::NoEditTriggers true QAbstractItemView::ExtendedSelection false QListView::Adjust 2 0 0 File Changes 6 9 9 9 9 0 0 0 0 Qt::ClickFocus false <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">This window will display the changes (diffs/deltas) made to the file currently selected in the file list. These are useful for providing meaningful commit messages.</p></body></html> true <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p></body></html> 6 0 0 0 0 Preferences ... Qt::Horizontal 381 24 <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Commit selected (checked) files</p></body></html> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Commit the selected (checked) files using the commit log message you provide in the top text area. Some VCS tools refer to this function as a submit, others a check-in.</p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">After commiting the selected files, if no commitable files remain in your repository, Qct will automatically exit.</p></body></html> C&ommit Close commitTextEntry fileListWidget commitPushButton cancelPushButton logHistComboBox selectAllPushButton sortComboBox filterLineEdit refreshPushButton prefPushButton cancelPushButton pressed() commitToolDialog reject() 620 654 359 646 qct/qctlib/gui_logic.py0000644000000000000000000012377111146115774015622 0ustar00usergroup00000000000000# gui_logic.py - Commit Tool # # Copyright 2006 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. from PyQt4 import QtCore, QtGui from qctlib.ui_dialog import Ui_commitToolDialog from qctlib.ui_preferences import Ui_prefDialog from qctlib.utils import formatPatchRichText, runProgram from qctlib.version import qct_version from qctlib.changeselect import ChangeDialog import os, sys, shutil, shlex class CommitTool(QtGui.QWidget): '''QCT commit tool GUI logic''' def __init__(self, vcs): '''Initialize the dialog window, fill with initial data''' QtGui.QWidget.__init__(self) self.maxHistCount = 8 self.vcs = vcs self.autoSelectTypes = self.vcs.getAutoSelectTypes() self.patchColors = {'std': 'black', 'new': '#009600', 'remove': '#C80000', 'head': '#C800C8'} self.fileCheckState = {} self.logHistory = [] self.changeSelectedFiles = [] self.showIgnored = False self.wrapList = False self.itemChangeEntered = False self.logTemplate = None self.ui = Ui_commitToolDialog() self.ui.setupUi(self) self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowContextHelpButtonHint) self.setWindowTitle("Qct Commit Tool %s" % qct_version) self.ui.histButton = None # Support for Mercurial Queues, and Stacked Git. If a patch is # applied, then the user is refreshing that patch, not commiting # to the underlying repository. self.patchRefreshMode = False self.commitButtonToolTip = "Commit selected (checked) files" if 'patchqueue' in self.vcs.capabilities() and self.vcs.isPatchQueue(): self.patchRefreshMode = True self.ui.commitMsgBox.setTitle(QtGui.QApplication.translate("commitToolDialog", "Patch Description: " + self.vcs.topPatchName(), None, QtGui.QApplication.UnicodeUTF8)) self.ui.commitTextEntry.setToolTip(QtGui.QApplication.translate("commitToolDialog", "Description of current top patch " + self.vcs.topPatchName(), None, QtGui.QApplication.UnicodeUTF8)) self.ui.commitPushButton.setText(QtGui.QApplication.translate("commitToolDialog", "Refresh Patch", None, QtGui.QApplication.UnicodeUTF8)) self.commitButtonToolTip = "Refresh selected files in patch " + self.vcs.topPatchName() self.ui.cancelPushButton.setText("Exit") # Support for merge commits (multiple parents for working directory) elif 'merge' in self.vcs.capabilities() and self.vcs.isMerge(): self.ui.commitMsgBox.setTitle(QtGui.QApplication.translate("commitToolDialog", "Merge Description", None, QtGui.QApplication.UnicodeUTF8)) self.ui.commitPushButton.setText(QtGui.QApplication.translate("commitToolDialog", "Commit Merge Results", None, QtGui.QApplication.UnicodeUTF8)) self.ui.selectAllPushButton.setEnabled(False) self.ui.fileListBox.setTitle(QtGui.QApplication.translate("commitToolDialog", "File list cannot be filtered when committing merge results", None, QtGui.QApplication.UnicodeUTF8)) # Recover persistent data settings = QtCore.QSettings('vcs', 'qct') settings.beginGroup('mainwindow') self.sortby = settings.value('sortby').toInt() # I don't understand why this check is required, seems like a # PyQt bug. It's returning a tuple (int, bool) if type(self.sortby) is not int: self.sortby = self.sortby[0] if settings.contains('size'): self.resize(settings.value('size').toSize()) self.move(settings.value('pos').toPoint()) self.ui.splitter.restoreState(settings.value('3waysplitter').toByteArray()) settings.endGroup() settings.beginGroup('commitLog') size = settings.beginReadArray('history') for i in xrange(0, size): settings.setArrayIndex(i) self.logHistory.append(settings.value('text').toString()) settings.endArray() settings.endGroup() self.ui.sortComboBox.setCurrentIndex(self.sortby) self.connect(self.ui.fileListWidget, QtCore.SIGNAL("customContextMenuRequested(const QPoint &)"), self.__contextMenu) self.__fillLogHistCombo() # Prepare for simple syntax highlighting self.ui.diffBrowser.setAcceptRichText(True) # Setup ESC to exit self.actionEsc = QtGui.QAction(self) self.actionEsc.setShortcut(QtGui.QKeySequence(self.tr("ESC"))) self.ui.commitTextEntry.addAction(self.actionEsc) self.connect(self.actionEsc, QtCore.SIGNAL("triggered()"), self.close) # Setup CTRL-O to trigger commit self.actionCtrlO = QtGui.QAction(self) self.actionCtrlO.setShortcut(QtGui.QKeySequence(self.tr("Ctrl+O"))) self.ui.commitTextEntry.addAction(self.actionCtrlO) self.connect(self.actionCtrlO, QtCore.SIGNAL("triggered()"), self.commitSelected) # Setup CTRL-R to trigger refresh self.actionCtrlR = QtGui.QAction(self) self.actionCtrlR.setShortcut(QtGui.QKeySequence(self.tr("Ctrl+R"))) self.ui.commitTextEntry.addAction(self.actionCtrlR) self.connect(self.actionCtrlR, QtCore.SIGNAL("triggered()"), self.on_refreshPushButton_pressed) # Setup CTRL-N to display next file self.actionCtrlN = QtGui.QAction(self) self.actionCtrlN.setShortcut(QtGui.QKeySequence(self.tr("Ctrl+N"))) self.ui.commitTextEntry.addAction(self.actionCtrlN) self.connect(self.actionCtrlN, QtCore.SIGNAL("triggered()"), self.displayNextFile) # Setup CTRL-U to select next file self.actionCtrlU = QtGui.QAction(self) self.actionCtrlU.setShortcut(QtGui.QKeySequence(self.tr("Ctrl+U"))) self.ui.commitTextEntry.addAction(self.actionCtrlU) self.connect(self.actionCtrlU, QtCore.SIGNAL("triggered()"), self.unselectAll) # Setup CTRL-] to scroll browser window self.actionPageDown = QtGui.QAction(self) self.actionPageDown.setShortcut(QtGui.QKeySequence(self.tr("CTRL+]"))) self.ui.commitTextEntry.addAction(self.actionPageDown) self.connect(self.actionPageDown, QtCore.SIGNAL("triggered()"), self.__pageDownBrowser) # Setup CTRL-[ to scroll browser window self.actionPageUp = QtGui.QAction(self) self.actionPageUp.setShortcut(QtGui.QKeySequence(self.tr("CTRL+["))) self.ui.commitTextEntry.addAction(self.actionPageUp) self.connect(self.actionPageUp, QtCore.SIGNAL("triggered()"), self.__pageUpBrowser) # Setup CTRL-F to clear filter self.actionCtrlF = QtGui.QAction(self) self.actionCtrlF.setShortcut(QtGui.QKeySequence(self.tr("CTRL+F"))) self.ui.commitTextEntry.addAction(self.actionCtrlF) self.connect(self.actionCtrlF, QtCore.SIGNAL("triggered()"), self.on_clearFilterButton_pressed) self.connect(self.ui.commitPushButton, QtCore.SIGNAL("clicked()"), self.commitSelected) self.ui.fileListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.__retrieveConfigurables() self.__rescanFiles() if not self.itemList: print "No uncommited changes" sys.exit() return self.__refreshFileList(True) self.__updateCommitButtonState() def reject(self): '''User has pushed the cancel button''' self.close() def __retrieveConfigurables(self): '''Run at startup and after the preferences dialog exits''' settings = QtCore.QSettings('vcs', 'qct') self.signoff = settings.value('signoff', QtCore.QVariant('')).toString() settings.beginGroup('fileList') self.showIgnored = settings.value('showIgnored', QtCore.QVariant(False)).toBool() self.wrapList = settings.value('wrapping', QtCore.QVariant(False)).toBool() settings.endGroup() settings.beginGroup('tools') self.histTool = str(settings.value('histTool', QtCore.QVariant('')).toString()) self.diffTool = str(settings.value('diffTool', QtCore.QVariant('')).toString()) self.editTool = str(settings.value('editTool', QtCore.QVariant('')).toString()) self.twowayTool = str(settings.value('twowayTool', QtCore.QVariant('')).toString()) settings.endGroup() # Disable the 'show ignored' feature if VCS does not support it (perforce) if 'ignore' not in self.vcs.capabilities(): self.showIgnored = False if self.ui.histButton and not self.histTool: self.ui.hboxlayout2.removeWidget(self.ui.histButton) self.ui.histButton = None elif self.histTool and not self.ui.histButton: self.ui.histButton = QtGui.QPushButton(self) self.ui.histButton.setObjectName("histButton") self.ui.histButton.setText("History") self.ui.hboxlayout2.insertWidget(1, self.ui.histButton) self.connect(self.ui.histButton, QtCore.SIGNAL("clicked()"), self.__history) if self.wrapList: self.ui.fileListWidget.setWrapping( True ) self.ui.fileListWidget.setFlow( QtGui.QListView.LeftToRight ) # self.ui.fileListWidget.setUniformItemSizes( True ) else: self.ui.fileListWidget.setWrapping( False ) self.ui.fileListWidget.setFlow( QtGui.QListView.TopToBottom ) # self.ui.fileListWidget.setUniformItemSizes( False ) def __history(self): '''User has clicked the visual history button''' if self.histTool: runProgram(self.histTool.split(' '), expectedexits=[0,1,255]) def __rescanFiles(self): '''Helper function which wraps progress bar functionality around the call to vcs.scanFiles() ''' if 'progressbar' in self.vcs.capabilities(): pb = QtGui.QProgressDialog() pb.setWindowTitle('Repository Scan') pb.setLabelText('Progress of repository scan') pb.setMinimum(0) pb.setMaximum(4) pb.setModal(True) pb.forceShow() pb.setValue(0) self.itemList = self.vcs.scanFiles(self.showIgnored, pb) else: self.itemList = self.vcs.scanFiles(self.showIgnored) self.__applyFilter() self.__sortList() def __applyFilter(self): filter = str(self.ui.filterLineEdit.text()) if filter: self.filteredList = [] for _item in self.itemList: if filter in _item[2:]: self.filteredList.append(_item) else: self.filteredList = self.itemList def on_clearFilterButton_pressed(self): self.ui.filterLineEdit.clear() def on_filterLineEdit_textChanged(self): self.__applyFilter() self.__refreshFileList(False) def __sortList(self): if self.sortby == 1: # filename self.filteredList.sort(lambda x, y: cmp(x[2:], y[2:])) elif self.sortby == 2: # status self.filteredList.sort() elif self.sortby == 3: # status self.filteredList.sort() self.filteredList.reverse() elif self.sortby == 4: # extension self.filteredList.sort(lambda x, y: cmp(os.path.splitext(x[2:])[1], os.path.splitext(y[2:])[1])) def __safeQMessageBox(self, title, text): try: ret = QtGui.QMessageBox.warning(self, title, text, QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel) except TypeError: # Older Qt versions (<4.1) have slightly different arguments ret = QtGui.QMessageBox.warning(self, title, text, QtGui.QMessageBox.Ok, QtGui.QMessageBox.Cancel) return ret def __contextMenu(self, Point): '''User has right clicked inside the file list window (or pressed the windows menu key). We determine which item is under the mouse and then present options. ''' item = self.ui.fileListWidget.itemAt(Point) if not item: return menuPos = self.ui.fileListWidget.mapToGlobal(Point) # Multi-selection context menu selectedItemList = self.ui.fileListWidget.selectedItems() if len(selectedItemList) > 1: allUnknowns = True for item in selectedItemList: itemName = str(item.text()) if itemName[0] not in ['?', 'I']: allUnknowns = False break if allUnknowns: menu = QtGui.QMenu() menu.addAction("Add all selected files to version control") menu.addAction("Delete all selected files") a = menu.exec_(menuPos) if a is None: return actionText = str(a.text()) if actionText.startswith('Delete all'): if self.__safeQMessageBox("File Deletion Warning", "Are you sure you want to delete all selected files?") != QtGui.QMessageBox.Ok: return for item in selectedItemList: itemName = str(item.text()) if os.path.isdir(itemName[2:]): shutil.rmtree(itemName[2:]) else: os.unlink(itemName[2:]) self.__rescanFiles() self.__refreshFileList(False) elif actionText.startswith('Add all'): selectedFileList = [] for item in selectedItemList: itemName = str(item.text()) selectedFileList.append(itemName[2:]) self.vcs.addFiles(selectedFileList) self.__rescanFiles() self.__refreshFileList(False) return else: menu = QtGui.QMenu() menu.addAction("Revert all selected files") if self.diffTool: menu.addAction("Visual diff") if self.patchRefreshMode: menu.addAction("Visual diff of all patch changes") a = menu.exec_(menuPos) if not a: return actionText = str(a.text()) if actionText.startswith('Revert'): if self.__safeQMessageBox("Revert Warning", "Are you sure you want to revert all selected files?") != QtGui.QMessageBox.Ok: return fileList = [] for item in selectedItemList: itemName = str(item.text()) fileList.append(itemName) self.vcs.revertFiles(fileList) self.__rescanFiles() self.__refreshFileList(False) elif actionText.startswith('Visual diff'): fileList = [] # Hack to get visual diff of all changes in the patch, not # just those in the working directory if actionText.endswith('all patch changes'): fileList = ['--rev', '-2' ] for item in selectedItemList: itemName = str(item.text()) fileList.append(itemName[2:]) runProgram(self.diffTool.split(' ') + fileList, expectedexits=[0,1,255]) self.__rescanFiles() self.__refreshFileList(False) return itemName = str(item.text()) targetType = itemName[0] targetFile = itemName[2:] # Context menu for unknown files (ignore masks or copy detection) if targetType in ['?', 'I']: menu = QtGui.QMenu() if targetType == '?' and 'ignore' in self.vcs.capabilities(): basename = os.path.basename(targetFile) # baz.ext dirname = os.path.dirname(targetFile) # foo/bar else '' ext = os.path.splitext(basename)[1] # .ext else '' menu.addAction("Add Ignore: %s" % targetFile) if dirname and ext: menu.addAction("Add Ignore: %s/*%s" % (dirname, ext)) if dirname: menu.addAction("Add Ignore: %s" % basename) if ext: menu.addAction("Add Ignore: *%s" % ext) if 'copy' in self.vcs.capabilities(): menu.addAction("%s is a copy of a revisioned file" % targetFile) if self.editTool: menu.addAction("Open in %s" % os.path.basename(self.editTool)) menu.addAction("Add to version control") menu.addAction("Delete %s" % targetFile) a = menu.exec_(menuPos) if a is not None: actionText = str(a.text()) if actionText.startswith('Add to'): self.vcs.addFiles([targetFile]) self.__rescanFiles() self.__refreshFileList(False) elif actionText.startswith('Add Ignore: '): self.vcs.addIgnoreMask(actionText[12:]) elif actionText.startswith('Open '): runProgram([self.editTool, targetFile], expectedexits=[0,1,255]) self.vcs.dirtyCache(targetFile) self.__refreshFileList(False) elif actionText.startswith('Delete '): if self.__safeQMessageBox("File Deletion Warning", "Are you sure you want to delete %s?" % targetFile) != QtGui.QMessageBox.Ok: return if os.path.isdir(targetFile): shutil.rmtree(targetFile) else: os.unlink(targetFile) else: self.__detectFileCopySource(targetFile) self.__rescanFiles() self.__refreshFileList(False) return # Context menu for rename events if targetType == '>': menu = QtGui.QMenu() menu.addAction("Revert rename back to %s" % targetFile) a = menu.exec_(menuPos) if not a: return self.__revertFile(itemName) self.__rescanFiles() self.__refreshFileList(False) return # Context menu for missing files (detect renames) if targetType == '!': menu = QtGui.QMenu() # Present unknown files as possible rename/move targets if self.unknownFileList and 'rename' in self.vcs.capabilities(): for u in self.unknownFileList: menu.addAction("%s was moved/renamed to %s" % (targetFile, u)) menu.addAction("Recover %s from revision control" % targetFile) a = menu.exec_(menuPos) if not a: return actionText = str(a.text()) if actionText.startswith('Recover '): self.__revertFile(itemName) else: l = len(targetFile) + 22 renameTarget = actionText[l:] self.vcs.fileMoveDetected(targetFile, renameTarget) self.__rescanFiles() self.__refreshFileList(False) # Context menu for files with merge conflicts if targetType == 'C': menu = QtGui.QMenu() menu.addAction("Revert %s to last revisioned state" % targetFile) if self.editTool: menu.addAction("Open in %s" % os.path.basename(self.editTool)) if self.diffTool: menu.addAction("Visual diff") a = menu.exec_(menuPos) if not a: return actionText = str(a.text()) if actionText.startswith('Open '): runProgram([self.editTool, targetFile], expectedexits=[0,1,255]) self.vcs.dirtyCache(targetFile) elif actionText.startswith('Revert '): self.__revertFile(itemName) self.__rescanFiles() elif actionText.startswith('Visual diff'): runProgram(self.diffTool.split(' ') + [ targetFile ], expectedexits=[0,1,255]) self.vcs.dirtyCache(targetFile) self.__refreshFileList(False) # Context menu for 'A' 'M' and 'R' (and 'a', 'm', 'r') if targetType in self.autoSelectTypes: menu = QtGui.QMenu() if targetFile in self.changeSelectedFiles: menu.addAction("Reset selection of changes") elif targetType == 'M': menu.addAction("Select changes to commit") if targetType not in ['a', 'm', 'r']: menu.addAction("Revert %s to last revisioned state" % targetFile) if targetType not in ['R', 'r'] and self.editTool: menu.addAction("Open in %s" % os.path.basename(self.editTool)) if targetType in ['M', 'A'] and self.diffTool: menu.addAction("Visual diff") if self.patchRefreshMode: menu.addAction("Visual diff of all patch changes") elif targetType in ['m', 'a'] and self.diffTool: menu.addAction("Visual diff of all patch changes") a = menu.exec_(menuPos) if not a: return actionText = str(a.text()) if actionText.startswith('Open '): runProgram([self.editTool, targetFile], expectedexits=[0,1,255]) self.vcs.dirtyCache(targetFile) elif actionText.startswith('Revert '): self.__revertFile(itemName) self.__rescanFiles() elif actionText.startswith('Visual diff'): if actionText.endswith('all patch changes'): args = ['--rev', '-2', targetFile ] else: args = [ targetFile ] runProgram(self.diffTool.split(' ') + args, expectedexits=[0,1,255]) self.vcs.dirtyCache(targetFile) elif actionText.startswith('Select '): self.__selectChanges(targetFile) elif actionText.startswith('Reset '): self.__resetChangeSelection(targetFile) self.__refreshFileList(False) def __selectChanges(self, workingFile): '''User would like to select changes made to this file for commit, unselected changes are left in working directory after commit or at exit. ''' self.vcs.dirtyCache(workingFile) workingCopy = '.qct/' + workingFile + '.orig' try: path = os.path.dirname(workingFile) os.makedirs('.qct/' + path) except OSError: pass try: os.remove(workingCopy) except OSError: pass try: os.rename(workingFile, workingCopy) except: return self.changeSelectedFiles.append(workingFile) try: self.vcs.generateParentFile(workingFile) if not self.twowayTool: dialog = ChangeDialog(workingCopy, workingFile) dialog.show() dialog.exec_() if not dialog.accepted: raise Exception # Copy permissions, times back to workingFile shutil.copystat(workingCopy, workingFile) return cmd = self.twowayTool if '%o' in cmd and '%m' in cmd: cmd = cmd.replace('%o', workingFile) cmd = cmd.replace('%m', workingCopy) cmd = cmd.replace('\\', '/') runProgram(shlex.split(cmd), expectedexits=[0,1,255]) else: runProgram([self.twowayTool, workingCopy, workingFile]) # Copy permissions, times back to workingFile shutil.copystat(workingCopy, workingFile) except: print "Change selection failed, returning working file" self.__resetChangeSelection(workingFile) def __resetChangeSelection(self, workingFile, deleteindex=True): '''Restore original working copy, clean up .qct/''' if deleteindex: i = self.changeSelectedFiles.index(workingFile) del self.changeSelectedFiles[i] self.vcs.dirtyCache(workingFile) workingCopy = '.qct/' + workingFile + '.orig' try: os.remove(workingFile) except OSError: pass os.rename(workingCopy, workingFile) try: path = os.path.dirname(workingFile) if path: os.removedirs('.qct/' + path) os.removedirs('.qct/') except OSError: pass def __updateCommitButtonState(self): '''Only enable the commit button if a valid log message exists and one or more files are selected ''' logMessage = self.ui.commitTextEntry.toPlainText() if (logMessage != self.logTemplate or self.patchRefreshMode) and self.getCheckedFiles(): self.ui.commitPushButton.setEnabled(True) self.ui.commitPushButton.setToolTip(QtGui.QApplication.translate("commitToolDialog", self.commitButtonToolTip, None, QtGui.QApplication.UnicodeUTF8)) else: self.ui.commitPushButton.setEnabled(False) self.ui.commitPushButton.setToolTip(QtGui.QApplication.translate("commitToolDialog", 'Disabled until file(s) are selected and a log message is entered', None, QtGui.QApplication.UnicodeUTF8)) def __revertFile(self, fileName): if self.__safeQMessageBox("Revert Warning", "Are you sure you want to revert %s?" % fileName[2:]) != QtGui.QMessageBox.Ok: return self.vcs.revertFiles([ fileName ]) def __detectFileCopySource(self, targetFile): '''The user has identified an unknown file as a copy of a revisioned file. Allow the user to select the copy source by opening a file selection dialog ''' ext = os.path.splitext(targetFile)[1] if ext: searchExtensions = '%s Files (*%s);;All Files (*)' % (ext[1:].capitalize(), ext) else: searchExtensions = 'All Files (*)' fileName = QtGui.QFileDialog.getOpenFileName(self, "Select copy source of %s" % targetFile, targetFile, searchExtensions) if not fileName.isEmpty(): self.vcs.fileCopyDetected(str(fileName), targetFile) def __saveLogMessage(self, logMessage): '''A new commit (or abort) has occurred, try to save the log message. If the message is a duplicate of a message already in the history, then move it to the top of the stack ''' if logMessage != self.logTemplate: if logMessage in self.logHistory: self.logHistory.remove(logMessage) self.logHistory.append(logMessage) if len(self.logHistory) > self.maxHistCount: del self.logHistory[0] def __fillLogHistCombo(self): '''Fill the log history drop-down box with the last N messages''' for log in self.logHistory: topLine = log.split('\n')[0] self.ui.logHistComboBox.insertItem(0, topLine) self.ui.logHistComboBox.setCurrentIndex(0) def on_prefPushButton_pressed(self): '''Preferences Dialog''' prefDialog = PrefDialog() prefDialog.show() prefDialog.exec_() oldIgnored = self.showIgnored self.__retrieveConfigurables() if self.showIgnored != oldIgnored: self.__rescanFiles() self.__refreshFileList(False) self.__updateCommitButtonState() @QtCore.pyqtSignature("int") def on_sortComboBox_activated(self, row): self.sortby = row self.__sortList() self.__refreshFileList(False) @QtCore.pyqtSignature("int") def on_logHistComboBox_activated(self, row): '''The user has selected a log entry from the history drop-down''' index = len(self.logHistory) - row - 1 self.ui.commitTextEntry.clear() self.ui.commitTextEntry.setFocus() self.ui.commitTextEntry.setPlainText(self.logHistory[index]) def closeEvent(self, e = None): '''Dialog is closing, save persistent state''' # Recover working directory first, priorities... for targetFile in self.changeSelectedFiles: self.__resetChangeSelection(targetFile,False) self.changeSelectedFiles = [] # Save off any aborted log message logMessage = self.ui.commitTextEntry.toPlainText() self.__saveLogMessage(logMessage) settings = QtCore.QSettings('vcs', 'qct') settings.beginGroup('mainwindow') settings.setValue("size", QtCore.QVariant(self.size())) settings.setValue("pos", QtCore.QVariant(self.pos())) settings.setValue("3waysplitter", QtCore.QVariant(self.ui.splitter.saveState())) settings.setValue("sortby", QtCore.QVariant(self.sortby)) settings.endGroup() settings.beginGroup('commitLog') settings.beginWriteArray('history') for i, log in enumerate(self.logHistory): settings.setArrayIndex(i) settings.setValue("text", QtCore.QVariant(log)) settings.endArray() settings.endGroup() settings.sync() if e is not None: e.accept() def __pageDownBrowser(self): '''Page Up the diff browser (Ctrl-])''' vs = self.ui.diffBrowser.verticalScrollBar() vs.triggerAction(QtGui.QAbstractSlider.SliderPageStepAdd) def __pageUpBrowser(self): '''Page Up the diff browser (Ctrl-[)''' vs = self.ui.diffBrowser.verticalScrollBar() vs.triggerAction(QtGui.QAbstractSlider.SliderPageStepSub) def __displaySelectedFile(self): '''Show status of currently selected file''' if not self.filteredList: return # Filtered list could be empty item = self.filteredList[ self.displayedRow ] deltaText = self.vcs.getFileStatus(item) #self.ui.diffBrowser.setPlainText(deltaText) self.ui.diffBrowser.setHtml(formatPatchRichText(deltaText, self.patchColors)) self.ui.diffBrowserBox.setTitle(item[2:] + " file status") def __refreshFileList(self, newCommitFlag): '''Refresh the file list, display status of first file''' if not self.itemList: print "No remaining uncommited changes" self.close() return if newCommitFlag: self.fileCheckState = {} if 'merge' in self.vcs.capabilities() and self.vcs.isMerge(): merge = True else: merge = False self.ui.fileListWidget.clear() self.unknownFileList = [] for itemName in self.filteredList: listItem = QtGui.QListWidgetItem(itemName) status = itemName[0] fileName = itemName[2:] if status == '?': self.unknownFileList.append(fileName) if newCommitFlag and status in self.autoSelectTypes: listItem.setCheckState(QtCore.Qt.Checked) self.fileCheckState[ fileName ] = True elif self.fileCheckState.has_key(fileName) and self.fileCheckState[ fileName ] == True: listItem.setCheckState(QtCore.Qt.Checked) else: listItem.setCheckState(QtCore.Qt.Unchecked) self.fileCheckState[ fileName ] = False if merge: listItem.setFlags(QtCore.Qt.ItemIsSelectable) self.ui.fileListWidget.addItem(listItem) # Display status (diff) of first item in list, and select it self.displayedRow = 0 self.__displaySelectedFile() item = self.ui.fileListWidget.item(0) self.ui.fileListWidget.setItemSelected(item, True) self.ui.fileListWidget.setCurrentItem(item) # Refresh log template if necessary if newCommitFlag or self.patchRefreshMode: self.logTemplate = self.vcs.getLogTemplate() # Prepare for new commit message if newCommitFlag: self.ui.commitTextEntry.clear() self.ui.commitTextEntry.setFocus() self.ui.commitTextEntry.setPlainText(self.logTemplate) self.__updateCommitButtonState() def on_commitTextEntry_textChanged(self): '''User has typed something in the commit text window''' self.__updateCommitButtonState() def unselectAll(self): '''Reset checked state of all files (Ctrl-U)''' self.fileCheckState = {} self.__refreshFileList(False) def getCheckedFiles(self): '''Helper function to build list of checked (selected) files''' checkedItemList = [] for item in self.itemList: fileName = item[2:] if self.fileCheckState[ fileName ] == True: checkedItemList.append(item) return checkedItemList def displayNextFile(self): '''User has hit CTRL-N''' self.displayedRow += 1 if self.displayedRow >= len(self.filteredList): self.displayedRow = 0 item = self.ui.fileListWidget.item(self.displayedRow) self.ui.fileListWidget.setCurrentItem(item) self.ui.fileListWidget.setItemSelected(item, True) self.ui.fileListWidget.scrollToItem(item) selectedItemList = self.ui.fileListWidget.selectedItems() for i in selectedItemList: if i is item: continue self.ui.fileListWidget.setItemSelected(i, False) self.__displaySelectedFile() def commitSelected(self): '''Commit selected files, then refresh the dialog for next commit''' checkedItemList = self.getCheckedFiles() if not checkedItemList: self.__safeQMessageBox("Commit Warning", "No files are selected, nothing to commit") self.ui.fileListWidget.setFocus() return logMessage = self.ui.commitTextEntry.toPlainText() if logMessage == self.logTemplate and not self.patchRefreshMode: self.__safeQMessageBox("Commit Warning", "No log message specified, aborting commit") self.ui.commitTextEntry.setFocus() return if self.signoff: logMessage += os.linesep + self.signoff msg = logMessage.toLocal8Bit() self.vcs.commitFiles(checkedItemList, msg) self.__saveLogMessage(logMessage) self.__fillLogHistCombo() # Put back unselected changes (original working copies) and # clean up .qct/ directory for targetFile in self.changeSelectedFiles: self.__resetChangeSelection(targetFile,False) self.changeSelectedFiles = [] self.__rescanFiles() self.__refreshFileList(True) def on_selectAllPushButton_pressed(self): '''(Un)Select All button has been pressed''' # Try to select all items changedFileState = False for item in self.filteredList: f = item[2:] if self.fileCheckState[ f ] == False: self.fileCheckState[ f ] = True changedFileState = True # If there were no un-selected items, toggle unselect them all if changedFileState == False: self.fileCheckState = { } self.__refreshFileList(False) def on_refreshPushButton_pressed(self): '''Refresh button pressed slot handler''' oldSelectState = self.fileCheckState self.fileCheckState = {} self.__rescanFiles() for item in self.filteredList: f = item[2:] if oldSelectState.has_key(f) and oldSelectState[ f ] == True: self.fileCheckState[ f ] = True else: self.fileCheckState[ f ] = False self.__refreshFileList(False) self.__updateCommitButtonState() def on_fileListWidget_itemActivated(self, item): '''The user has activated a list item, we toggle its check state''' # These will trigger cell change signals if item.checkState() == QtCore.Qt.Checked: item.setCheckState(QtCore.Qt.Unchecked) else: item.setCheckState(QtCore.Qt.Checked) def on_fileListWidget_itemChanged(self, item): '''The user has modified the check state of an item, If the item was part of a select group we set them all to the checked state of the modified item.''' if self.itemChangeEntered: return self.itemChangeEntered = True if item.checkState() == QtCore.Qt.Checked: selectedItemList = self.ui.fileListWidget.selectedItems() if item in selectedItemList: for si in selectedItemList: fileName = str(si.text())[2:] si.setCheckState(QtCore.Qt.Checked) self.fileCheckState[ fileName ] = True else: fileName = str(item.text())[2:] item.setCheckState(QtCore.Qt.Checked) self.fileCheckState[ fileName ] = True else: selectedItemList = self.ui.fileListWidget.selectedItems() if item in selectedItemList: for si in selectedItemList: fileName = str(si.text())[2:] si.setCheckState(QtCore.Qt.Unchecked) self.fileCheckState[ fileName ] = False else: fileName = str(item.text())[2:] item.setCheckState(QtCore.Qt.Unchecked) self.fileCheckState[ fileName ] = False self.__updateCommitButtonState() self.itemChangeEntered = False def on_fileListWidget_itemClicked(self, item): '''The user has clicked on a list item''' row = self.ui.fileListWidget.row(item) if row != -1 and self.filteredList and row != self.displayedRow: self.displayedRow = row self.__displaySelectedFile() self.__updateCommitButtonState() def on_fileListWidget_itemSelectionChanged(self): '''The user has selected a list item''' row = self.ui.fileListWidget.currentRow() if row != -1 and self.filteredList and row != self.displayedRow: if row >= len(self.filteredList): row = 0 self.displayedRow = row self.__displaySelectedFile() class PrefDialog(QtGui.QDialog): '''QCT Preferences Dialog''' def __init__(self): QtGui.QDialog.__init__(self) self.ui = Ui_prefDialog() self.ui.setupUi(self) settings = QtCore.QSettings('vcs', 'qct') self.signoff = settings.value('signoff', QtCore.QVariant('')).toString() settings.beginGroup('fileList') self.showIgnored = settings.value('showIgnored', QtCore.QVariant(False)).toBool() self.wrapList = settings.value('wrapping', QtCore.QVariant(False)).toBool() settings.endGroup() settings.beginGroup('tools') self.diffTool = settings.value('diffTool', QtCore.QVariant('')).toString() self.histTool = settings.value('histTool', QtCore.QVariant('')).toString() self.editTool = settings.value('editTool', QtCore.QVariant('')).toString() self.twowayTool = settings.value('twowayTool', QtCore.QVariant('')).toString() settings.endGroup() # Disable wrap feature for Qt < 4.2 try: from PyQt4 import pyqtconfig except ImportError: # The Windows installed PyQt4 does not support pyqtconfig, but # does support wrapping, etc. So we will leave this feature # enabled if we fail to import pyqtconfig. # self.ui.wrapListCheckBox.setEnabled(False) pass else: pyqtconfig = pyqtconfig.Configuration() if pyqtconfig.qt_version < 0x40200: self.wrapList = False self.ui.wrapListCheckBox.setEnabled(False) self.ui.wrapListCheckBox.setToolTip(QtGui.QApplication.translate("wrapListCheckBox", "This feature requires Qt >= 4.2", None, QtGui.QApplication.UnicodeUTF8)) self.ui.ignoredButton.setChecked(self.showIgnored) self.ui.wrapListCheckBox.setChecked(self.wrapList) self.ui.diffToolEdit.setText(self.diffTool) self.ui.histToolEdit.setText(self.histTool) self.ui.mergeToolEdit.setText(self.twowayTool) self.ui.editToolEdit.setText(self.editTool) self.ui.signoffTextEdit.setPlainText(self.signoff) def on_aboutPushButton_pressed(self): QtGui.QMessageBox.about(self, 'Qct Commit Tool', "

Qct " + qct_version + """

\n
Copyright © 2007 Steve Borho <steve@borho.org>
\n

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation.

""") def accept(self): self.histTool = str(self.ui.histToolEdit.text()) self.diffTool = str(self.ui.diffToolEdit.text()) self.twowayTool = str(self.ui.mergeToolEdit.text()) self.editTool = str(self.ui.editToolEdit.text()) self.wrapList = self.ui.wrapListCheckBox.isChecked() self.showIgnored = self.ui.ignoredButton.isChecked() settings = QtCore.QSettings('vcs', 'qct') settings.setValue('signoff', QtCore.QVariant(self.ui.signoffTextEdit.toPlainText())) settings.beginGroup('fileList') settings.setValue('showIgnored', QtCore.QVariant(self.ui.ignoredButton.isChecked())) settings.setValue('wrapping', QtCore.QVariant(self.ui.wrapListCheckBox.isChecked())) settings.endGroup() settings.beginGroup('tools') settings.setValue('histTool', QtCore.QVariant(self.histTool)) settings.setValue('diffTool', QtCore.QVariant(self.diffTool)) settings.setValue('editTool', QtCore.QVariant(self.editTool)) settings.setValue('twowayTool', QtCore.QVariant(self.twowayTool)) settings.endGroup() settings.sync() self.close() qct/qctlib/patches.py0000644000000000000000000004416711146115774015311 0ustar00usergroup00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('*** '): continue if line.startswith('==='): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 qct/qctlib/preferences.ui0000644000000000000000000004310211146115774016134 0ustar00usergroup00000000000000 prefDialog 0 0 366 211 0 0 Qct Preferences true 0 Externals <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Supply the name of an external editor, it will be offered as an option in the context menu of appropriate files.</p></body></html> Editor: editToolEdit <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Specify a two-way merge program that can be used to select changes in your working files for commit. Kompare, meld, and kdiff3 are good examples on Linux.</p></body></html> Two-Way Merge: mergeToolEdit <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">External editor</p></body></html> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Merge tool used for change selection. If you leave this field blank, qct will use a built in change selection feature.</p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Example command line: mymergetool "%m" "%o"</p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">%m will be substituted with the modified file from your working copy.</p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">%o will be substituted with the original unmodified file name (and also the merge output file).</p></body></html> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p></body></html> Diff Viewer diffToolEdit <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p></body></html> History Viewer histToolEdit <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Specify a diff browser command line which is supported by your revision control tool. It will be passed the name of the file you wish to browse, so the VCS must supply the actual diffs. For example: </p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">hg vdiff</p></body></html> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Specify a command line that Qct can call to retrieve visual diffs from your revision control tool. Qct will pass the list of selected files to this command line.</p></body></html> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Enter the command line to launch your revision control tools's history browser. For example:</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">hg view</p></body></html> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Specify a command line that Qct can call to spawn a graphical history browser for your repository.</p></body></html> Sign Off 6 9 9 9 9 0 0 <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The message you enter here will be appended to every log message you commit with Qct. The Mercurial back-end has a separate facility to configure a sign-off message on a per-repository basis. Please consult the INSTALL file for details.</p></body></html> File List 6 9 9 9 9 <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Flow file names from left to right with wrapping</p></body></html> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">This toggles the layout of the file list. If checked, the files will flow from left to right with wrapping at the right edge and scrolling at the bottom edge. If unchecked, the files will be in a single row from top to bottom.</p></body></html> Wrap List <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Show files normally ignored by revision control</p></body></html> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal; text-decoration:none;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">This button toggles display of unrevisioned files which are typically ignored by your revision control system (via ignore masks). If you commit one of these files it will be automatically added to revision control.</p></body></html> Show Ignored Qt::Vertical 101 16 6 0 0 0 0 Qt::Horizontal 40 20 About... Qt::Horizontal 40 20 QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok diffToolEdit editToolEdit mergeToolEdit tabWidget signoffTextEdit wrapListCheckBox ignoredButton aboutPushButton buttonBox accepted() prefDialog accept() 207 181 124 177 buttonBox rejected() prefDialog reject() 299 184 31 177 qct/qctlib/select.ui0000644000000000000000000000470011146115774015113 0ustar00usergroup00000000000000 ChangeDialog 0 0 495 356 Select Changes for Commit Qt::Horizontal 40 20 Keep Shelve Qt::Horizontal 40 20 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok buttonBox accepted() ChangeDialog accept() 248 254 157 274 buttonBox rejected() ChangeDialog reject() 316 260 286 274 qct/qctlib/utils.py0000644000000000000000000001610211146115774015006 0ustar00usergroup00000000000000# Helper classes for VCS back-end code # # Copyright 2006 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. import os, sys from PyQt4 import QtCore, QtGui from string import split import errno def findInSystemPath(filename): '''Search for an executable in the system path''' paths = split(os.environ['PATH'], os.path.pathsep) for path in paths: fullName = os.path.join(path, filename) if os.path.exists(fullName): return os.path.abspath(os.path.join(path, filename)) return None def isBinary(filename): """Return true iff the given filename is binary. Raises an EnvironmentError if the file does not exist or cannot be accessed. """ fin = open(filename, 'rb') try: CHUNKSIZE = 1024 while 1: chunk = fin.read(CHUNKSIZE) if '\0' in chunk: # found null byte return 1 if len(chunk) < CHUNKSIZE: break # done finally: fin.close() return 0 def scanDiffOutput(diffStream): '''Scan output of diff, collect lists and patch hash''' patchHash = { } addedList = [ ] removedList = [ ] modifiedList = [ ] # A typical diff header looks like this: # diff -r 52b82ffc6695 -r 52b82ffc6695 qctlib/dialog.ui # --- a/qctlib/dialog.ui Wed Dec 27 14:02:36 2006 -0600 # +++ b/qctlib/dialog.ui Wed Dec 27 08:39:45 2006 -0600 fileName = '' patchStarted = False patchContents = [ ] lines = diffStream.split('\n') for line in lines: if line.startswith('diff '): # Start of next patch if patchStarted and patchContents: patchHash[fileName] = '\n'.join(patchContents) patchContents = [ ] patchStarted = True maybe_git, fileName = tuple(line.split(' ')[-3 : -1]) # Convert / to \ on Windows, diff always reports / if "--git" in maybe_git: fileName = os.sep.join(fileName.split('/')[1:]) else: fileName = os.sep.join(fileName.split('/')) elif line.startswith('Binary file '): patchStarted = True fileName = line.split(' ')[-3] # Convert / to \ on Windows, diff always reports / fileName = os.sep.join(fileName.split('/')) elif line.startswith('--- '): words = line.split(' ') fname = words[1].split('\t')[0] if fname == "/dev/null": addedList.append(fileName) elif line.startswith('+++ '): words = line.split(' ') fname = words[1].split('\t')[0] if fname == "/dev/null": removedList.append(fileName) elif fileName not in addedList: modifiedList.append(fileName) if patchStarted: patchContents.append(line) if fileName and patchContents: patchHash[fileName] = '\n'.join(patchContents) return (addedList, removedList, modifiedList, patchHash) import subprocess # Thank you, hgct authors class ProgramError(Exception): def __init__(self, progStr, error): self.progStr = progStr self.error = error.rstrip() def __str__(self): return self.progStr + ': ' + self.error def runProgram(prog, input=None, expectedexits=[0]): if type(prog) is str: progStr = prog else: progStr = ' '.join(prog) try: pop = subprocess.Popen(prog, shell = type(prog) is str, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, stdin=subprocess.PIPE) except OSError, e: raise ProgramError(progStr, e.strerror) if input != None: pop.stdin.write(input) pop.stdin.close() try: out = pop.stdout.read() except IOError: out = '' pass while True: try: code = pop.wait() except OSError, e: if e.errno == errno.EINTR: continue else: raise e else: break if code not in expectedexits: # Windows does not use uchar return values code = code % 256 if code not in expectedexits: print >>sys.stderr, "Error code %d not expected" % code raise ProgramError(progStr, out) return out def runProgramStderr(prog, input=None, expectedexits=[0]): if type(prog) is str: progStr = prog else: progStr = ' '.join(prog) try: pop = subprocess.Popen(prog, shell = type(prog) is str, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) except OSError, e: raise ProgramError(progStr, e.strerror) if input != None: pop.stdin.write(input) pop.stdin.close() try: out = pop.stdout.read() except IOError: out = '' try: err = pop.stderr.read() except IOError: err = '' while True: try: code = pop.wait() except OSError, e: if e.errno == errno.EINTR: continue else: raise e else: break if code not in expectedexits: # Windows does not use uchar return values code = code % 256 if code not in expectedexits: print >>sys.stderr, "Error code %d not expected" % code raise ProgramError(progStr, out) return (out, err) def formatPatchRichText(patch, colors): '''Syntax highlight patches based on first character of each line''' ret = ['
']
    prev = 'header'
    for l in patch.split('\n'):
        if l: c = l[0]
        else: c = ' '

        # Allow VCS code to insert RichText in the diff header by
        # preceding RichText lines with %.
        if prev == 'header':
            if c == '%':
                ret.extend([str(l[1:]), os.linesep])
                continue
            else:
                prev = ' '

        if l.startswith('= '):
            ret.extend(['' + l[2:] + '
' + '']) prev = ' ' continue elif c != prev: if c == '+': style = 'new' elif c == '-': style = 'remove' elif c == '@': style = 'head' else: style = 'std' ret.extend(['']) prev = c # Escape patch text, make it HTML safe try: line = QtCore.Qt.escape(QtCore.QString.fromLocal8Bit(l)) except UnicodeEncodeError: line = '!Unicode Encoding Error!' ret.extend([line, os.linesep]) ret.append('
') retstring = QtCore.QString() for s in ret: retstring += s return retstring qct/qctlib/vcs/__init__.py0000644000000000000000000000001611146115774016175 0ustar00usergroup00000000000000# placeholder qct/qctlib/vcs/bzr.py0000644000000000000000000002156211146115774015244 0ustar00usergroup00000000000000# Bazaar VCS back-end code for qct # # Copyright 2006 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. from qctlib.utils import runProgram from tempfile import mkstemp import os class qctVcsBzr: def initRepo(self, argv): '''Initialize your revision control system, open repository''' # Verify we have a valid repository output = runProgram(['bzr', 'root'], expectedexits=[0,3]) words = output.split(' ') if len(words) == 1: self.repoRoot = output[:-len(os.linesep)] return 0 else: print output return -1 def capabilities(self): '''Return a list of optional capabilities supported by this VCS''' # bazaar does not seem to support file copies or marking moves # which were initially done outside of revision detection (using mv) return ('ignore', 'progressbar') def generateParentFile(self, workingFile): '''The GUI needs this file's parent revision in place so the user can select individual changes for commit (basically a revert) ''' runProgram(['bzr', 'revert', '--no-backup', workingFile]) def addIgnoreMask(self, newIgnoreMask): '''The user has provided a new ignoreMask to be added to revision control''' runProgram(['bzr', 'ignore', newIgnoreMask]) print "Added '%s' to ignore mask" % newIgnoreMask def getLogTemplate(self): '''Request default log message template from VCS''' logFileName = self.repoRoot + "/.commit.template" try: f = open(logFileName) text = f.read() f.close() return text except IOError: return '' def getAutoSelectTypes(self): '''Return annotations of file types which should be automatically selected when a new commit is started''' return ['A', 'M', 'R', '>'] def dirtyCache(self, fileName): '''The GUI would like us to forget cached data for this file''' for itemName in self.diffCache.keys(): if itemName[2:] == fileName: del self.diffCache[itemName] def scanFiles(self, showIgnored, pb = None): '''Request scan for all commitable files from VCS''' # Called at startup, when 'Refresh' button is pressed, or when # showIgnored toggled. self.diffCache = { } statusOutput = runProgram(['bzr', 'status']) if pb: pb.setValue(1) recs = statusOutput.split(os.linesep) recs.pop() # remove last entry (which is '') itemList = [] fileList = [] type = '' for line in recs: if line == 'added:': type = 'A' elif line == 'modified:': type = 'M' elif line == 'unknown:': type = '?' elif line == 'removed:': type = 'R' elif line == 'renamed:': type = '>' elif line[-1] == '/': line = '' # nop, just skipping this directory else: # Prevent files from showing up w/ two status if line[2:] not in fileList: itemList.append(type + " " + line[2:]) fileList.append(line[2:]) if pb: pb.setValue(2) if showIgnored: statusOutput = runProgram(['bzr', 'ls', '--ignored']) recs = statusOutput.split(os.linesep) recs.pop() # remove last entry (which is '') for fileName in recs: itemList.append("I " + fileName) if pb: pb.setValue(3) return itemList def getFileStatus(self, itemName): '''Request file deltas from VCS''' annotation = itemName[0] fileName = itemName[2:] bFileName = "%" + fileName + "" if annotation == 'A': note = bFileName + " has been added to VCS, but has never been commited" if self.diffCache.has_key(itemName): text = self.diffCache[itemName] else: text = runProgram(['bzr', 'diff', fileName], expectedexits=[0,1])[:-len(os.linesep)] self.diffCache[itemName] = text return note + os.linesep + text elif annotation == 'R': note = bFileName + " has been marked for deletion, but has not yet been commited" if self.diffCache.has_key(itemName): text = self.diffCache[itemName] else: text = runProgram(['bzr', 'diff', fileName], expectedexits=[0,1])[:-len(os.linesep)] self.diffCache[itemName] = text return note + os.linesep + text elif annotation == 'M': note = bFileName + " has been modified in your working directory" if self.diffCache.has_key(itemName): text = self.diffCache[itemName] else: text = runProgram(['bzr', 'diff', fileName], expectedexits=[0,1])[:-len(os.linesep)] self.diffCache[itemName] = text return note + os.linesep + text elif annotation == '?': note = bFileName + " is not currently revisioned, will be added to VCS if commited" if self.diffCache.has_key(itemName): text = self.diffCache[itemName] else: text = runProgram(['diff', '-u', '/dev/null', fileName], expectedexits=[0,1,2]) if not text: text = "" self.diffCache[itemName] = text return note + os.linesep + text elif annotation == 'I': note = bFileName + " is usually ignored, but will be added to VCS if commited" + os.linesep if self.diffCache.has_key(itemName): text = self.diffCache[itemName] else: text = runProgram(['diff', '-u', '/dev/null', fileName], expectedexits=[0,1,2]) if not text: text = "" self.diffCache[itemName] = text return note + os.linesep + text elif annotation == '>': return "%Rename event: " + fileName else: return "Unknown file type " + annotation def commitFiles(self, selectedFileList, logMsgText): '''Commit selected files''' # Files in list are annotated (A, M, etc) so this function can # mark files for add or delete as necessary before instigating the commit. commitFileNames = [] renameFiles = [] for f in selectedFileList: annotation = f[0] fileName = f[2:] if annotation == '?' or annotation == 'I': print "Adding %s to revision control" % fileName runProgram(['bzr', 'add', fileName]) commitFileNames.append(fileName) elif annotation == '>': print "Commit rename: %s" % fileName renameFiles += fileName.split(' => ') else: commitFileNames.append(fileName) # Renamed files may be on the modified list as well, so we add # them at the end to prevent duplicates for f in renameFiles: if f not in commitFileNames: commitFileNames.append(f) (fd, filename) = mkstemp() file = os.fdopen(fd, "w+b") file.write(logMsgText) file.close() runProgram(['bzr', 'commit', '-F', filename] + commitFileNames) print "%d file(s) commited: %s" % (len(selectedFileList), ', '.join(commitFileNames)) def addFiles(self, selectedFileList): '''Add selected files to version control''' runProgram(['bzr', 'add'] + selectedFileList) def revertFiles(self, selectedFileList): '''Revert selected files to last revisioned state''' revertFileNames = [] for f in selectedFileList: annotation = f[0] fileName = f[2:] if annotation == 'R': print "deleted %s recovered from revision control" % fileName revertFileNames.append(fileName) elif annotation == '>': print "rename %s recovered from revision control" % fileName renameFiles = fileName.split(' => ') revertFileNames.append(renameFiles[0]) elif annotation == 'A': print "added %s forgot from revision control" % fileName revertFileNames.append(fileName) elif annotation == 'M': print "modifications to %s reverted" % fileName revertFileNames.append(fileName) else: print "File %s not reverted" % fileName if len(revertFileNames): runProgram(['bzr', 'revert'] + revertFileNames) print "%d file(s) reverted: %s" % (len(revertFileNames), ', '.join(revertFileNames)) else: print "No revertable files" # vim: tw=120 qct/qctlib/vcs/cg.py0000644000000000000000000002227711146115774015044 0ustar00usergroup00000000000000# Cogito VCS back-end code for qct # # Copyright 2006 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. from qctlib.utils import runProgram, isBinary from tempfile import mkstemp import os class qctVcsCg: def initRepo(self, argv): '''Initialize your revision control system, open repository''' self.stateNames = { 'M' : 'modified', 'R' : 'removed', '!' : 'missing', '?' : 'unknown' } self.repoRoot = self.detect_root() if not self.repoRoot: print 'No Git/Cogito repository found' return -1 return 0 def detect_root(self): path = os.getcwd() while path != '/': if os.path.exists(os.path.join(path, ".git")): return path path = os.path.dirname(path) return None def capabilities(self): '''Return a list of optional capabilities supported by this VCS''' return ('ignore', 'progressbar') def generateParentFile(self, workingFile): '''The GUI needs this file's parent revision in place so the user can select individual changes for commit (basically a revert) ''' runProgram(['cg', 'restore', '-f', workingFile]) def addIgnoreMask(self, newIgnoreMask): '''The user has provided a new ignoreMask to be added to revision control''' try: f = open(os.path.join(self.repoRoot, ".gitignore"), 'a') f.write(newIgnoreMask) f.write('\n') f.close() print "Added '%s' to ignore mask" % newIgnoreMask except IOError, e: print "Unable to add '%s' to ignore mask" % newIgnoreMask print e def getLogTemplate(self): '''Request default log message template from VCS''' logFileName = self.repoRoot + "/.commit.template" try: f = open(logFileName) text = f.read() f.close() return text except IOError: return '' def getAutoSelectTypes(self): '''Return annotations of file types which should be automatically selected when a new commit is started''' return ['A', 'M', 'R'] def dirtyCache(self, fileName): '''The GUI would like us to forget cached data for this file''' for itemName in self.diffCache.keys(): if itemName[2:] == fileName: del self.diffCache[itemName] def scanFiles(self, showIgnored, pb = None): '''Request scan for all commitable files from VCS''' # Called at startup and when 'Refresh' button is pressed self.diffCache = {} itemList = [] if pb: pb.setValue(1) if showIgnored: extra = ['-x'] else: extra = [] recs = runProgram(['cg', 'status'] + extra).split(os.linesep) recs.pop() # remove eoln if pb: pb.setValue(2) # Skip header lines by looking for blank line lookingForBlank = True for line in recs: if lookingForBlank: if not line: lookingForBlank = False continue status = line[0] fname = line[2:] if status == 'M': # modified itemList.append('M ' + fname) elif status == 'A': # added itemList.append('A ' + fname) elif status == '?': # unknown itemList.append('? ' + fname) elif status == '!': # missing itemList.append('! ' + fname) elif status == 'D': # deleted itemList.append('R ' + fname) elif status in ('m'): # Skip these files, they are not commitable pass else: print "Cogito returned unexpected status %s" % status if pb: pb.setValue(3) return itemList def __getWorkingDirChanges(self, fileName, type): if self.diffCache.has_key(fileName): return self.diffCache[fileName] # For revisioned files, we use cg diff if type in ['A', 'M', 'R']: os.environ['PAGER'] = 'cat' text = runProgram(['cg', 'diff', fileName], expectedexits=[0,1]) self.diffCache[fileName] = text return text elif type in ['!']: # Missing files can be retrieved with cat text = runProgram(['cg', 'admin-cat', fileName], expectedexits=[0,1]) self.diffCache[fileName] = text return text elif type in ['?', 'I']: # For unrevisioned files, we return file contents if os.path.isdir(fileName): text = " " fnames = os.listdir(fileName) text += os.linesep + ' ' + '\n '.join(fnames) elif isBinary(fileName): text = " " else: f = open(fileName) text = f.read() f.close() self.diffCache[fileName] = text return text else: return "Unknown file type " + type def getFileStatus(self, itemName): '''Request file deltas from VCS''' type = itemName[0] fileName = itemName[2:] text = self.__getWorkingDirChanges(fileName, type) # Useful shorthand vars. Leading lines beginning with % are treated as RTF bFileName = "%" + fileName + "" noteLineSep = os.linesep + '%' if type == 'A': note = bFileName + " has been added to git, but has never been commited." return note + os.linesep + text elif type == 'M': note = bFileName + " has been modified in your working directory." return note + os.linesep + text elif type == '?': note = bFileName + " is not currently tracked. If commited, it will be added to revision control." return note + os.linesep + "= Unrevisioned File Contents" + os.linesep + text elif type == 'I': note = bFileName + " is usually ignored, but will be added to revision control if commited" return note + os.linesep + text elif type == 'R': note = bFileName + " has been marked for deletion, or renamed, but has not yet been commited" note += noteLineSep + "The file can be recovered by reverting it to it's last revisioned state." return note + os.linesep + "= Removed File Diffs" + os.linesep + text elif type == '!': note = bFileName + " was tracked but is now missing. If commited, it will be marked as removed in git." note += noteLineSep + "The file can be recovered by reverting it to it's last revisioned state." return note + os.linesep + "= Contents of Missing File" + os.linesep + text else: return "Unknown file type " + type def commitFiles(self, selectedFileList, logMsgText): '''Commit selected files''' # Files in list are annotated (A, M, etc) so this function can # mark files for add or delete as necessary before instigating the commit. commitFileNames = [] renameFiles = [] for f in selectedFileList: annotation = f[0] fileName = f[2:] commitFileNames.append(fileName) if annotation in ('?', 'I'): runProgram(['cg', 'add', fileName]) elif type == '!': removeFileList.append(fileName) (fd, filename) = mkstemp() file = os.fdopen(fd, "w+b") file.write(logMsgText) file.close() runProgram(['cg', 'commit', '-M', filename] + commitFileNames) print "%d file(s) commited: %s" % (len(selectedFileList), ', '.join(commitFileNames)) return def addFiles(self, selectedFileList): '''Add selected files to version control''' runProgram(['cg', 'add'] + selectedFileList) def revertFiles(self, selectedFileList): '''Revert selected files to last revisioned state''' revertFileNames = [] for f in selectedFileList: annotation = f[0] fileName = f[2:] if annotation in ['R', '!']: print "deleted %s recovered from revision control" % fileName revertFileNames.append(fileName) elif annotation == '>': print "rename %s recovered from revision control" % fileName renameFiles = fileName.split(' => ') revertFileNames.append(renameFiles[0]) elif annotation == 'A': print "added %s forgot from revision control" % fileName revertFileNames.append(fileName) elif annotation == 'M': print "modifications to %s reverted" % fileName revertFileNames.append(fileName) else: print "File %s not reverted" % fileName if len(revertFileNames): runProgram(['cg', 'restore', '-f'] + revertFileNames) print "%d file(s) reverted: %s" % (len(revertFileNames), ', '.join(revertFileNames)) else: print "No revertable files" return # vim: tw=120 qct/qctlib/vcs/cvs.py0000644000000000000000000002165711146115774015247 0ustar00usergroup00000000000000# CVS back-end code for qct # # Copyright 2007 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. # Usage Notes: # # * Assumes you have a working cvs command line tool # * Assumes you have CVSROOT setup appropriately # * Assumes there is no passphrase required for rsh/ssh access # * Does not support listing ignored files (cvs seems broken in this regard) from qctlib.utils import runProgram, isBinary from tempfile import mkstemp import os class qctVcsCvs: def initRepo(self, argv): '''Initialize your revision control system, open repository''' if not os.path.exists('CVS/'): print "No CVS repository found" return -1 self.stateNames = { 'M' : 'modified', 'R' : 'removed', '!' : 'missing', '?' : 'unknown' } return 0 def capabilities(self): '''Return a list of optional capabilities supported by this VCS''' return [ 'ignore', 'progressbar' ] def generateParentFile(self, workingFile): '''The GUI needs this file's parent revision in place so the user can select individual changes for commit (basically a revert) ''' runProgram(['cvs', 'update', '-C', workingFile]) def addIgnoreMask(self, newIgnoreMask): '''The user has provided a new ignoreMask to be added to revision control''' try: f = open('.cvsignore', 'a') f.write(newIgnoreMask) f.write('\n') f.close() print "Added '%s' to ignore mask" % newIgnoreMask except IOError, e: print "Unable to add '%s' to ignore mask" % newIgnoreMask print e def getLogTemplate(self): '''Request default log message template from VCS''' logFileName = os.path.expanduser('~/.commit.template') try: f = open(logFileName) text = f.read() f.close() return text except IOError: return '' def getAutoSelectTypes(self): '''Return annotations of file types which should be automatically selected when a new commit is started''' return ['A', 'M', 'R'] def dirtyCache(self, fileName): '''The GUI would like us to forget cached data for this file''' if self.wdDiffCache.has_key(fileName): del self.wdDiffCache[fileName] def scanFiles(self, showIgnored, pb = None): '''Request scan for all commitable files from VCS, with optional progress bar ''' # Called at startup and when 'Refresh' button is pressed self.wdDiffCache = {} itemList = [] if pb: pb.setValue(1) # Provides ARM, same as diff, plus unknown ? and missing ! statusOutput = runProgram(['cvs', '-qn', 'update']) recs = statusOutput.split(os.linesep) recs.pop() # remove last entry (which is '') if pb: pb.setValue(2) nextFileMissing = False for line in recs: status = line[0] fname = line[2:] if line.endswith("' was lost"): nextFileMissing = True elif nextFileMissing: itemList.append('! ' + fname) nextFileMissing = False elif status not in ['U', 'D', 'P']: itemList.append(line) if pb: pb.setValue(3) return itemList def __getWorkingDirChanges(self, fileName, type): if self.wdDiffCache.has_key(fileName): return self.wdDiffCache[fileName] # For revisioned files, we use cvs diff if type in ['A', 'M']: text = runProgram(['cvs', 'diff', '-du', fileName], expectedexits=[0,1]) self.wdDiffCache[fileName] = text return text if type in ['R', '!']: text = 'Deleted file, unable to retrieve contents' # TODO self.wdDiffCache[fileName] = text return text # For unrevisioned files, we return file contents if type in ['?', 'I']: if os.path.isdir(fileName): text = " " fnames = os.listdir(fileName) text += os.linesep + ' ' + '\n '.join(fnames) elif isBinary(fileName): text = " " else: f = open(fileName) text = f.read() f.close() self.wdDiffCache[fileName] = text return text else: return "Unknown file type " + type def getFileStatus(self, itemName): '''Request file deltas from VCS''' type = itemName[0] fileName = itemName[2:] text = self.__getWorkingDirChanges(fileName, type) # Useful shorthand vars. Leading lines beginning with % are treated as RTF bFileName = "%" + fileName + "" noteLineSep = os.linesep + '%' if type == 'A': note = bFileName + " has been added to cvs, but has never been commited." return note + os.linesep + text elif type == 'M': note = bFileName + " has been modified in your working directory." return note + os.linesep + text elif type == '?': note = bFileName + " is not currently tracked. If commited, it will be added to revision control." return note + os.linesep + "= Unrevisioned File Contents" + os.linesep + text elif type == 'I': note = bFileName + " is usually ignored, but will be added to revision control if commited" return note + os.linesep + text elif type == 'R': note = bFileName + " has been marked for deletion, or renamed, but has not yet been commited" note += noteLineSep + "The file can be recovered by reverting it to it's last revisioned state." return note + os.linesep + "= Removed File Diffs" + os.linesep + text elif type == '!': note = bFileName + " was tracked but is now missing. If commited, it will be marked as removed in cvs." note += noteLineSep + "The file can be recovered by reverting it to it's last revisioned state." return note + os.linesep + "= Contents of Missing File" + os.linesep + text else: return "Unknown file type " + type def commitFiles(self, selectedFileList, logMsgText): '''Commit selected files''' # Files in list are annotated (A, M, etc) so this function can # mark files for add or delete as necessary before instigating the commit. commitFileNames = [] addFileList = [] binaryAddFileList = [] removeFileList = [] for f in selectedFileList: type = f[0] fileName = f[2:] commitFileNames.append(fileName) if type in ['?', 'I']: if isBinary(fileName): binaryAddFileList.append(fileName) else: addFileList.append(fileName) elif type == '!': removeFileList.append(fileName) if binaryAddFileList: runProgram(['cvs', 'add', '-kb'] + addFileList) print "Added %d binary file(s) to revision control: %s" % (len(addFileList), ', '.join(addFileList)) if addFileList: runProgram(['cvs', 'add'] + addFileList) print "Added %d file(s) to revision control: %s" % (len(addFileList), ', '.join(addFileList)) if removeFileList: runProgram(['cvs', 'delete'] + removeFileList) print "Removed %d file(s) from revision control: %s" % (len(removeFileList), ', '.join(removeFileList)) (fd, filename) = mkstemp() file = os.fdopen(fd, "w+b") file.write(logMsgText) file.close() runProgram(['cvs', 'commit', '-F', filename] + commitFileNames) print "%d file(s) commited: %s" % (len(commitFileNames), ', '.join(commitFileNames)) def addFiles(self, selectedFileList): '''Add selected files to version control''' runProgram(['cvs', 'add'] + selectedFileList) def revertFiles(self, selectedFileList): '''Revert selected files to last revisioned state''' revertFileNames = [] for f in selectedFileList: type = f[0] fileName = f[2:] if type in ['R', '!', 'M']: prevState = self.stateNames[type] print "%s recovered to last revisioned state (was %s)" % (fileName, prevState) revertFileNames.append(fileName) elif type == 'A': print "%s removed from revision control (was added)" % fileName revertFileNames.append(fileName) else: print "File %s not reverted" % fileName if revertFileNames: runProgram(['cvs', 'update', '-C'] + revertFileNames) else: print "No revertable files" # vim: tw=120 qct/qctlib/vcs/git.py0000644000000000000000000001255211146115774015231 0ustar00usergroup00000000000000# GIT VCS back-end code for qct # # Copyright 2006 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. import sys, os, re from qctlib.utils import runProgram class qctVcsGit: def initRepo(self, argv): '''Initialize your revision control system, open repository''' if not os.environ.has_key('GIT_DIR'): os.environ['GIT_DIR'] = '.git' if not os.environ.has_key('GIT_OBJECT_DIRECTORY'): os.environ['GIT_OBJECT_DIRECTORY'] = os.environ['GIT_DIR'] + '/objects' if not (os.path.exists(os.environ['GIT_DIR']) and os.path.exists(os.environ['GIT_DIR'] + '/refs') and os.path.exists(os.environ['GIT_OBJECT_DIRECTORY'])): print "Git archive not found." print "Make sure that the current working directory contains a '.git' directory, or\nthat GIT_DIR is set appropriately." return -1 return 0 def capabilities(self): '''Return a list of optional capabilities supported by this VCS''' # Git does not support explicit renames and moves, AFAIK return ('ignore', 'patchqueue') def addIgnoreMask(self, newIgnoreMask): '''The user has provided a new ignoreMask to be added to revision control''' # TODO (add to .gitignore?) def getLogTemplate(self): '''Request default log message template from VCS''' return '' def getAutoSelectTypes(self): '''Return annotations of file types which should be automatically selected when a new commit is started''' return ['A', 'M', 'R'] def isPatchQueue(self): '''Return true if Stacked Git Queue patches are applied''' return False def generateParentFile(self, workingFile): '''The GUI needs this file's parent revision in place so the user can select individual changes for commit (basically a revert) ''' # TODO: Verify this gets the parent revision runProgram(['git', 'cat', workingFile]) def dirtyCache(self, fileName): '''The GUI would like us to forget cached data for this file''' pass def scanFiles(self, showIgnored): '''Request scan for all commitable files from VCS''' # Called at startup, when 'Refresh' button is pressed, or when # showIgnored toggled. list = runProgram(['git-diff-files', '--name-status', '-z']).split('\0') list.pop() fileList = [] nameList = [] while len(list): name = list.pop() nameList.append(name) type = list.pop() fileList.append(type + " " + name) runXargsStyle(['git-update-index', '--remove', '--'], nameList) # The returned file list will be annotated by type return fileList def getFileStatus(self, itemName): '''Request file deltas from VCS''' annotation = itemName[0] fileName = itemName[2:] if annotation == 'A': return fileName + " has been added to VCS or is a rename target, but has never been commited" elif annotation == '?': return fileName + " is not currently tracked, will be added to VCS if commited" elif annotation == '!': return fileName + " was tracked but is now missing, will be removed from VCS if commited" elif annotation == 'I': return fileName + " is usually ignored, but will be added to VCS if commited" elif annotation == 'R': return fileName + " has been marked for deletion, or renamed, but has not yet been commited" elif annotation == 'M': return runProgram(['git-diff-cache', '-p', '-M', '--cached', 'HEAD', fileName]) else: return "Unknown file type " + annotation def commitFiles(self, selectedFileList, logMsgText): '''Commit selected files''' # Files in list are annotated (A, M, etc) so this function can # mark files for add or delete as necessary before instigating the commit. commitFileNames = [] for f in selectedFileList: annotation = f[0] fileName = f[2:] if annotation == '?' or annotation == 'I': print "Adding %s to revision control" % fileName #runProgram(['hg', 'add', fileName]) elif annotation == '!': print "Removing %s from revision control" % fileName #runProgram(['hg', 'rm', fileName]) commitFileNames.append(fileName) #runProgram(['hg', 'commit', '-l', '-'] + commitFileNames, logMsgText) print "%d file(s) commited: %s" % (len(selectedFileList), ', '.join(commitFileNames)) # You could call sys.exit(), if this is running inside a plugin/extension return def addFiles(self, selectedFileList): '''Add selected files to version control''' runProgram(['git', 'add'] + selectedFileList) def revertFiles(self, selectedFileList): print "Git revert is currently unsupported" return def runXargsStyle(origProg, args): steps = range(10, len(args), 10) prog = origProg[:] prev = 0 for i in steps: prog.extend(args[prev:i]) runProgram(prog) prog = origProg[:] prev = i prog.extend(args[prev:]) runProgram(prog) # vim: tw=120 qct/qctlib/vcs/hg.py0000644000000000000000000006104111146115774015041 0ustar00usergroup00000000000000# Mercurial VCS back-end code for qct # # Copyright 2006 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. import os, sys from stat import * from qctlib.utils import * from tempfile import mkstemp from StringIO import StringIO def findHg(): path = os.environ["PATH"].split(os.pathsep) for d in path: if os.name == "nt": pathexts = os.environ["PATHEXT"].split(os.pathsep) for ext in pathexts: exepath = os.path.join(d, 'hg' + ext) if os.access(exepath, os.X_OK): try: runProgram([exepath, 'version']) return exepath except: pass else: exepath = os.path.join(d, 'hg') if os.access(exepath, os.X_OK): try: runProgram([exepath, 'version']) return exepath except: pass return None class qctVcsHg: def initRepo(self, argv, hgdispatch=None, username=None): '''Initialize your revision control system, open repository''' self.commitname = username # If we're running from the qct.py extension, we can call directly into mercurial if hgdispatch: self.commands = hgdispatch else: self.commands = None self.hg_exe = findHg() if not self.hg_exe: print "Unable to find hg (.exe, .bat, .cmd) in your path" return -1 # Verify we have a valid repository (out, err) = self.hgcmd(['root'], okresults=[0,255]) self.repoRoot = out.split('\n')[0] if err.startswith('abort'): print err return -1 self.parents = self.hgcmd(['parents', '-q'])[0].splitlines() if os.getcwd() == self.repoRoot: self.runningFromRoot = True else: self.runningFromRoot = False self.stateNames = { 'M' : 'modified', 'R' : 'removed', '!' : 'missing', '?' : 'unknown' } self.capList = [ 'ignore', # VCS supports ignore masks (addIgnoreMask) 'copy', # VCS supports revisioned copying of files (fileMoveDetected) 'rename', # VCS supports revisioned renames (fileMoveDetected) 'symlink', # VCS supports symlinks 'patchqueue', # VCS supports patch queue (isPatchQueue, topPatchName) 'progressbar', # back-end supports a progress bar 'merge'] # VCS supports merges (isMerge) self.cmdLineOptions = [] # To enable an auto-matic sign-off message: # [qct] # signoff = Sign-Off: Steve Borho self.signOff = self.hgcmd(['showconfig', 'qct.signoff'])[0] # Determine if this repository has any applied Mercurial Queue patches (output, err) = self.hgcmd(['qheader'], okresults=[0,1,255]) if err and "unknown command" in err: self.isPatchQ = False return 0 if output and 'o patches applied' in output: self.isPatchQ = False return 0 # Patches only make sense from repository root if not self.runningFromRoot: print "Changing context to repository root: " + self.repoRoot os.chdir(self.repoRoot) self.runningFromRoot = True self.isPatchQ = True return 0 def hgcmd(self, args, okresults=[0], binary=False): if self.commands: # Use mercurial library directly ostream = StringIO() errstream = StringIO() ret = None try: sys.stdout = ostream sys.stderr = errstream sys.stdin = StringIO() ret = self.commands.dispatch(args) finally: sys.stdin = sys.__stdin__ sys.stdout = sys.__stdout__ sys.stderr = sys.__stderr__ if ret and ret not in okresults: ret = ret % 256 if ret not in okresults: print "Cmd: hg", ' '.join(args), 'failed with code', ret outstreams = (ostream.getvalue(), errstream.getvalue()) ostream.close() errstream.close() return outstreams else: (out, err) = runProgramStderr([self.hg_exe] + args, expectedexits=okresults) if os.linesep != '\n' and not binary: out = out.replace(os.linesep, '\n') err = err.replace(os.linesep, '\n') return (out, err) def pluginOptions(self, opts): '''The Mercurial extension is passing along -I/-X command line options''' for epath in opts['exclude']: self.cmdLineOptions += ['-X', epath] for ipath in opts['include']: self.cmdLineOptions += ['-I', ipath] if 'user' in opts: for name in opts['user']: self.commitname = name def capabilities(self): '''Return a list of optional capabilities supported by this VCS''' return self.capList def generateParentFile(self, workingFile): '''The GUI needs this file's parent revision in place so the user can select individual changes for commit (basically a revert) ''' self.hgcmd(['revert', '--no-backup', workingFile]) def addIgnoreMask(self, newIgnoreMask): '''The user has provided a new ignoreMask to be added to revision control''' # Read existing .hgignore (possibly empty) globString = 'syntax: glob\n' try: f = open(os.path.join(self.repoRoot, '.hgignore'), 'rb') iLines = f.readlines() except IOError: iLines = [] else: f.close() # fixup eoln to unix style for search and insert if iLines and iLines[0].endswith('\r\n'): iLines = [line[:-2] + '\n' for line in iLines] doseoln = True else: doseoln = False # Find 'syntax: glob' line, add at end if not found if globString in iLines: line = iLines.index(globString) else: iLines.append(globString) line = len(iLines) - 1 if line == 0: iLines.append('') # Insert new mask after 'syntax: glob' line iLines.insert(line + 1, newIgnoreMask + '\n') # replace original eoln if doseoln: iLines = [line[:-1] + '\r\n' for line in iLines] try: f = open(os.path.join(self.repoRoot, '.hgignore'), 'wb') f.writelines(iLines) f.close() print "Added '%s' to ignore mask" % newIgnoreMask except IOError, e: print "Unable to add '%s' to ignore mask" % newIgnoreMask print e shell_notify(os.path.join(self.repoRoot, '.hgignore')) def fileMoveDetected(self, origFileName, newFileName): '''User has associated an unknown file with a missing file, describing a move/rename which occurred outside of revision control''' self.hgcmd(['mv', '--after', origFileName, newFileName]) print "Recording move of %s to %s" % (origFileName, newFileName) def fileCopyDetected(self, origFileName, newFileName): '''User has associated an unknown file with an existing file, describing a copy which occurred outside of revision control''' self.hgcmd(['cp', '--after', origFileName, newFileName]) print "Recording copy of %s to %s" % (origFileName, newFileName) def getLogTemplate(self): '''Request default log message template from VCS''' # If this repository has a patch queue with applied patches, then the # user is not commiting a changeset. they are refreshing the top patch. # So we put the current patch's description in the edit window. if self.isPatchQ: qheader = self.hgcmd(['qheader'], okresults=[0,1,255])[0] return qheader[:-1] try: f = open(os.path.join(self.repoRoot, '.commit.template'), 'r') text = f.read() f.close() except IOError: text = '' return text def getAutoSelectTypes(self): '''Return annotations of file types which should be automatically selected when a new commit is started''' if self.isPatchQ: return ['A', 'M', 'R', 'a', 'm', 'r'] else: return ['A', 'M', 'R'] def isPatchQueue(self): '''Return true if Mercurial Queue patches are applied''' return self.isPatchQ def isMerge(self): '''Return true if working directory has two parents''' return len(self.parents) > 1 def topPatchName(self): '''Return name of top patch (being refreshed)''' output = self.hgcmd(['qtop'], okresults=[0,1])[0] return output[:-1] def dirtyCache(self, fileName): '''The GUI would like us to forget cached data for this file''' if self.wdDiffCache.has_key(fileName): del self.wdDiffCache[fileName] if self.patchDiffCache.has_key(fileName): del self.patchDiffCache[fileName] def scanFiles(self, showIgnored, pb = None): '''Request scan for all commitable files from VCS, with optional progress bar ''' # Called at startup, when 'Refresh' button is pressed, or when showIgnored toggled. self.patchDiffCache = {} self.wdDiffCache = {} # Cache changes in the working directory (parse and store hg diff). The paths reported # by diff are always relative to the repo root, so if we're running outside of the root # directory there is no point in trying to pre-cache diffs. if self.runningFromRoot and len(self.parents) == 1: diff = self.hgcmd(['diff', '--show-function'] + self.cmdLineOptions)[0] (addedList, removedList, modifiedList, self.wdDiffCache) = scanDiffOutput(diff) if pb: pb.setValue(1) # Provides ARM, same as diff, plus unknown ? and missing ! statusOutput = self.hgcmd(['status'] + self.cmdLineOptions + ['.'])[0] recs = statusOutput.split('\n') recs.pop() # remove last entry (which is '') if pb: pb.setValue(2) if showIgnored: statusOutput = self.hgcmd(['status', '-i'] + self.cmdLineOptions + ['.'])[0] recs += statusOutput.split('\n') recs.pop() # remove last entry (which is '') if pb: pb.setValue(3) annotatedFileList = [ ] workingDirList = [ ] for fileName in recs: workingDirList.append(fileName[2:]) annotatedFileList.append(fileName) if pb: pb.setValue(4) if self.isPatchQ: # Capture changes in the current patch (parse and store hg tip) modifiedPList = self.hgcmd(['tip', '--debug', '--template', "{files}"])[0].split() addedPList = self.hgcmd(['tip', '--debug', '--template', "{file_adds}"])[0].split() removedPList = self.hgcmd(['tip', '--debug', '--template', "{file_dels}"])[0].split() self.filesinpatch = modifiedPList + addedPList + removedPList # Add patch files which did not show up in `hg status` for f in addedPList: if f not in workingDirList: annotatedFileList.append('a ' + f) for f in removedPList: if f not in workingDirList: annotatedFileList.append('r ' + f) for f in modifiedPList: if f in removedPList: continue if f in addedPList: continue if f not in workingDirList: annotatedFileList.append('m ' + f) return annotatedFileList def __getPatchChanges(self, filename, type): if filename not in self.filesinpatch: hgpathname = '/'.join(filename.split(os.sep)) if hgpathname not in self.filesinpatch: return '--- Not yet included in patch ---' if self.patchDiffCache.has_key(filename): return self.patchDiffCache[filename] if type in ['A', 'a', 'M', 'm']: self.patchDiffCache[filename] = self.hgcmd(['qdiff', filename])[0] return self.patchDiffCache[filename] if type in ['R', 'r']: self.patchDiffCache[filename] = self.hgcmd(['cat', '-r', '-2', filename])[0] return self.patchDiffCache[filename] return 'unknown patch state!' def __getWorkingDirChanges(self, fileName, type): if self.wdDiffCache.has_key(fileName): return self.wdDiffCache[fileName] # For symlinks, we return the link data if type not in ['R', '!']: lmode = os.lstat(fileName)[ST_MODE] if S_ISLNK(lmode): text = "Symlink: %s -> %s" % (fileName, os.readlink(fileName)) self.wdDiffCache[fileName] = text return text # For revisioned files, we use hg diff if type in ['A', 'M', 'R']: if len(self.parents) > 1: text = "\n= Diff to first parent %s\n" % self.parents[0] text += self.hgcmd(['diff', '--show-function', fileName])[0] otherparentrev = self.parents[1].split(':')[0] text += "\n= Diff to second parent %s\n" % self.parents[1] text += self.hgcmd(['diff', '--show-function', '--rev', otherparentrev, fileName])[0] else: text = self.hgcmd(['diff', '--show-function', fileName])[0] self.wdDiffCache[fileName] = text return text # For unrevisioned files, we return file contents if type in ['?', 'I']: if isBinary(fileName): text = " " else: f = open(fileName) text = f.read() f.close() self.wdDiffCache[fileName] = text return text # For missing files, we use hg cat if type == '!': text = self.hgcmd(['cat', fileName], binary=True)[0] if not text: text = " " elif '\0' in text: text = " " % (len(text) / 1024) self.wdDiffCache[fileName] = text return text else: return "Unknown file type " + type def getFileStatus(self, itemName): '''Request file deltas from VCS''' if self.isPatchQ: return self._getPatchFileStatus(itemName) type = itemName[0] fileName = itemName[2:] text = self.__getWorkingDirChanges(fileName, type) linesep = '\n' # Useful shorthand vars. Leading lines beginning with % are treated as RTF bFileName = "%" + fileName + "" noteLineSep = linesep + '%' if type == 'A': note = bFileName + " has been added to revision control or is a rename target, but has never been commited." return note + linesep + text elif type == 'M': if len(self.parents) > 1: note = bFileName + " has been merged in your working directory." else: note = bFileName + " has been modified in your working directory." return note + linesep + text elif type == '?': note = bFileName + " is not currently tracked. If commited, it will be added to revision control." return note + linesep + "= Unrevisioned File Contents" + linesep + text elif type == 'I': note = bFileName + " is usually ignored, but will be added to revision control if commited" return note + linesep + text elif type == 'R': note = bFileName + " has been marked for deletion, or renamed, but has not yet been commited" note += noteLineSep + "The file can be recovered by reverting it to it's last revisioned state." return note + linesep + "= Removed File Diffs" + linesep + text elif type == '!': note = bFileName + " was tracked but is now missing. If commited, it will be marked as removed in revision control." note += noteLineSep + "The file can be recovered by reverting it to it's last revisioned state." return note + linesep + "= Contents of Missing File" + linesep + text else: return "Unknown file type " + type def _getPatchFileStatus(self, itemName): '''Get status of a file, which may have patch diffs, and may have working directory diffs''' type = itemName[0] fileName = itemName[2:] # Useful shorthand vars. Leading lines beginning with % are treated as RTF bFileName = "%" + fileName + "" linesep = '\n' noteLineSep = linesep + '%' if type == 'A': note = bFileName + " has been added to the working directory, but has not been included in this patch." note += noteLineSep + "If reverted, this file will return to an unrevisioned state." wtext = self.__getWorkingDirChanges(fileName, type) return note + linesep + "= Added File Diffs" + linesep + wtext elif type == 'a': note = bFileName + " is a new file provided by this patch. " note += noteLineSep + "Reverting this file has no effect, it must be removed from the patch first." ptext = self.__getPatchChanges(fileName, type) return note + linesep + "= Added File Diffs" + linesep + ptext elif type == '?': note = bFileName + " is not currently tracked. If commited, it will appear to be provided by this patch. " note += noteLineSep + "Reverting this file has no effect." wtext = self.__getWorkingDirChanges(fileName, type) return note + linesep + "= Unrevisioned File Contents" + linesep + wtext elif type == 'I': note = bFileName + " is usually ignored, but will be recorded as provided by this patch if commited. " note += noteLineSep + "Reverting this file has no effect." wtext = self.__getWorkingDirChanges(fileName, type) return note + linesep + "= Unrevisioned File Contents" + linesep + wtext elif type == '!': note = bFileName + " was tracked but is now missing, will be marked as removed by this patch if commited. " note += noteLineSep + "If reverted, this file will be recovered to last revisioned state." wtext = self.__getWorkingDirChanges(fileName, type) return note + linesep + "= Contents of Missing File" + linesep + wtext elif type == 'R': note = bFileName + " has been marked for deletion in your working directory, but has not yet been commited. " note += noteLineSep + "If reverted, this file will be recovered to it's last revisioned state." wtext = self.__getWorkingDirChanges(fileName, type) return note + linesep + "= Removed File Diffs" + linesep + wtext elif type == 'r': note = bFileName + " is deleted by this patch" note += noteLineSep + "If you remove this file from the patch, it will appear removed in your working dir, " note += noteLineSep + "at which point you can revert it to it's last revisioned state." ptext = self.__getPatchChanges(fileName, type) return note + linesep + "= Removed File Diffs" + linesep + ptext elif type == 'M': wtext = self.__getWorkingDirChanges(fileName, type) ptext = self.__getPatchChanges(fileName, type) if ptext: note = bFileName + " has changes recorded in the patch, and further changes in the working directory " note += noteLineSep + "If reverted, only the working directory changes will be removed. " note += noteLineSep + "If you refresh without this file, all changes will be left in your working directory." status = note + linesep + "= Working Directory Diffs" + linesep + wtext status += linesep + "= Patch Diffs" + linesep + ptext else: note = bFileName + " has changes in the working directory that are not yet included in this patch." note += noteLineSep + "If reverted, the working directory diffs will be removed." status = note + linesep + "= Working Directory Diffs" + linesep + wtext return status elif type == 'm': note = bFileName + " is modified by this patch. There are no further changes in the working directory so " note += noteLineSep + "reverting this file will have no effect. If you remove this file from the patch " note += noteLineSep + "these modifications will be left in the working directory." ptext = self.__getPatchChanges(fileName, type) return note + linesep + "= Patch Diffs" + linesep + ptext else: return "Unknown file type " + type def commitFiles(self, selectedFileList, logMsgText): '''Commit selected files''' # Files in list are annotated (A, M, etc) so this function can # mark files for add or delete as necessary before instigating the commit. commitFileNames = [] addFileList = [] removeFileList = [] for f in selectedFileList: type = f[0] fileName = f[2:] commitFileNames.append(fileName) if type in ['?', 'I']: addFileList.append(fileName) elif type == '!': removeFileList.append(fileName) if addFileList: self.hgcmd(['add'] + addFileList) print "Added %d file(s) to revision control" % len(addFileList) if removeFileList: self.hgcmd(['rm'] + removeFileList) print "Removed %d file(s) from revision control" % len(removeFileList) if self.signOff: logMsgText += '\n' + self.signOff (fd, filename) = mkstemp() file = os.fdopen(fd, "w+b") file.write(logMsgText) file.close() if self.isPatchQ: self.hgcmd(['qrefresh', '-l', filename] + commitFileNames) print self.topPatchName() + " refreshed with %d file(s)" % len(commitFileNames) elif len(self.parents) > 1: if self.commitname: self.hgcmd(['commit', '-u', self.commitname, '-l', filename]) else: self.hgcmd(['commit', '-l', filename]) print "Merge results commited" else: if self.commitname: self.hgcmd(['commit', '-u', self.commitname, '-l', filename] + commitFileNames) else: self.hgcmd(['commit', '-l', filename] + commitFileNames) print "%d file(s) commited" % len(commitFileNames) os.unlink(filename) shell_notify(commitFileNames) def addFiles(self, selectedFileList): '''Add selected files to version control''' self.hgcmd(['add'] + selectedFileList) shell_notify(selectedFileList) def revertFiles(self, selectedFileList): '''Revert selected files to last revisioned state''' revertFileNames = [] for f in selectedFileList: type = f[0] fileName = f[2:] if type in ['R', '!', 'M']: prevState = self.stateNames[type] print "%s recovered to last revisioned state (was %s)" % (fileName, prevState) revertFileNames.append(fileName) elif type == 'A': print "%s removed from revision control (was added)" % fileName revertFileNames.append(fileName) else: print "File %s not reverted" % fileName if revertFileNames: self.hgcmd(['revert'] + revertFileNames) shell_notify(revertFileNames) else: print "No revertable files" def shell_notify(paths): ''' Refresh Windows shell when file states change. This allows the explorer to refresh their icons based on their new state ''' if not os.name == 'nt': return try: from win32com.shell import shell, shellcon for path in paths: abspath = os.path.abspath(path) pidl, ignore = shell.SHILCreateFromPath(abspath, 0) shell.SHChangeNotify(shellcon.SHCNE_UPDATEITEM, shellcon.SHCNF_IDLIST | shellcon.SHCNF_FLUSHNOWAIT, pidl, None) except ImportError: pass # vim: tw=120 qct/qctlib/vcs/mtn.py0000644000000000000000000003061711146115774015246 0ustar00usergroup00000000000000# Monotone VCS back-end code for qct # # Copyright 2006 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. # Usage Notes: # # User must specify a get_passphrase() in their monotonerc in order for # this back-end to commit files. from qctlib.utils import runProgram, findInSystemPath, scanDiffOutput, isBinary from tempfile import mkstemp from stat import * import os class qctVcsMtn: def initRepo(self, argv): '''Initialize your revision control system, open repository''' self.mtn_exe = 'mtn' output = runProgram([self.mtn_exe, 'ls', 'unknown'], expectedexits=[0,1]) if output.startswith('mtn: misuse: workspace required'): print "No Monotone repository found" return -1 # Verify we have a valid repository self.stateNames = { 'M' : 'modified', 'R' : 'removed', '!' : 'missing', '?' : 'unknown' } self.capList = [ 'ignore', # VCS supports ignore masks (addIgnoreMask) 'rename', # VCS supports revisioned renames (fileMoveDetected) 'progressbar'] # back-end supports a progress bar return 0 def capabilities(self): '''Return a list of optional capabilities supported by this VCS''' return self.capList def generateParentFile(self, workingFile): '''The GUI needs this file's parent revision in place so the user can select individual changes for commit (basically a revert) ''' runProgram([self.mtn_exe, 'revert', workingFile]) def addIgnoreMask(self, newIgnoreMask): '''The user has provided a new ignoreMask to be added to revision control Requires 'ignore' capability. ''' # (probably too simple) Glob-to-Regexp regexp = '^' for char in newIgnoreMask: if char == '*': regexp += '.*' elif char == '.': regexp += '\.' else: regexp += char regexp += '$' if not os.path.exists('.mtn-ignore'): print 'No ignore file found, unable to add %s' % regexp return try: f = open('.mtn-ignore', 'a') f.write(regexp) f.write('\n') f.close() print "Added regexp '%s' to ignore mask" % regexp except IOError, e: print "Unable to add '%s' to ignore mask" % regexp print e pass def fileMoveDetected(self, origFileName, newFileName): '''User has associated an unknown file with a missing file, describing a move/rename which occurred outside of revision control Requires 'rename' capability ''' runProgram([self.mtn_exe, 'rename', origFileName, newFileName]) def getLogTemplate(self): '''Request default log message template from VCS''' return '' def getAutoSelectTypes(self): '''Return annotations of file types which should be automatically selected when a new commit is started''' return ['A', 'M', 'R', '>'] def dirtyCache(self, fileName): '''The GUI would like us to forget cached data for this file''' if self.wdDiffCache.has_key(fileName): del self.wdDiffCache[fileName] def scanFiles(self, showIgnored, pb = None): '''Request scan for all commitable files from VCS, with optional progress bar ''' # Called at startup, when 'Refresh' button is pressed, or when showIgnored toggled. self.wdDiffCache = {} inventory = runProgram([self.mtn_exe, 'automate', 'inventory']).split(os.linesep)[:-1] if pb: pb.setValue(1) self.fileState = {} itemList = [] renameSrc = {} renameDest = {} listedFiles = [] for line in inventory: state = line[0:3] srcRename = line[4] destRename = line[6] fileName = line[8:] if state == ' ': # Skip unchanged files continue self.fileState[fileName] = state if srcRename != '0': renameSrc[srcRename] = fileName if destRename != '0': renameDest[destRename] = fileName if state[2] == 'I': # Ignored by lua-hook if showIgnored: itemList.append('I ' + fileName) listedFiles.append(fileName) elif state[2] == 'P': # patched (modified) itemList.append('M ' + fileName) listedFiles.append(fileName) elif state[2] == 'U': # Unknown if state[0] == 'D': # Dropped itemList.append('R ' + fileName) listedFiles.append(fileName) else: itemList.append('? ' + fileName) listedFiles.append(fileName) elif state[2] == 'M': # Missing itemList.append('! ' + fileName) listedFiles.append(fileName) elif state[2] == ' ': if state[1] == 'A': # Added itemList.append('A ' + fileName) listedFiles.append(fileName) elif state[0] == 'D': itemList.append('R ' + fileName) listedFiles.append(fileName) elif state[0] == 'R' or state[1] == 'R': pass else: print '%s [%s] is uncharacterized!' % (fileName, state) if pb: pb.setValue(2) # Find rename pairs self.renameTarget = {} for k in renameSrc.keys(): src = renameSrc[k] tgt = renameDest[k] self.renameTarget[src] = tgt if src not in listedFiles: itemList.append('> ' + src) listedFiles.append(src) if pb: pb.setValue(3) return itemList def __getWorkingDirChanges(self, fileName, type): if self.wdDiffCache.has_key(fileName): return self.wdDiffCache[fileName] # For symlinks, we return the link data if type not in ['R', '!', '>']: lmode = os.lstat(fileName)[ST_MODE] if S_ISLNK(lmode): text = "Symlink: %s -> %s" % (fileName, os.readlink(fileName)) self.wdDiffCache[fileName] = text return text # For revisioned files, we use hg diff if type in ['A', 'M', 'R']: text = runProgram([self.mtn_exe, 'diff', fileName]) self.wdDiffCache[fileName] = text return text # For unrevisioned files, we return file contents if type in ['?', 'I']: if os.path.isdir(fileName): text = " " fnames = os.listdir(fileName) text += os.linesep + ' ' + '\n '.join(fnames) elif isBinary(fileName): text = " " else: f = open(fileName) text = f.read() f.close() self.wdDiffCache[fileName] = text return text if type == '>': target = self.renameTarget[fileName] text = 'Rename event: %s [%s] -> %s [%s]' % (fileName, self.fileState[fileName], target, self.fileState[target]) self.wdDiffCache[fileName] = text return text # For missing files, we use mtn cat if type == '!': if self.fileState[fileName][1] == 'A': text = " " self.wdDiffCache[fileName] = text return text text = runProgram([self.mtn_exe, 'cat', fileName]) if not text: text = " " elif '\0' in text: text = " " % (len(text) / 1024) self.wdDiffCache[fileName] = text return text else: return "Unknown file type " + type def getFileStatus(self, itemName): '''Request file deltas from VCS''' type = itemName[0] fileName = itemName[2:] text = self.__getWorkingDirChanges(fileName, type) # Useful shorthand vars. Leading lines beginning with % are treated as RTF bFileName = '%' + '%s [%s]' % (fileName, self.fileState[fileName]) noteLineSep = os.linesep + '%' if type == 'A': note = bFileName + " has been added to revision control, but has never been commited." return note + os.linesep + text elif type == 'M': note = bFileName + " has been modified in your working directory." return note + os.linesep + text elif type == '?': note = bFileName + " is not currently tracked. If commited, it will be added to revision control." return note + os.linesep + "= Unrevisioned File Contents" + os.linesep + text elif type == 'I': note = bFileName + " is usually ignored, but will be added to revision control if commited" return note + os.linesep + text elif type == 'R': note = bFileName + " has been marked for deletion, but has not yet been commited" note += noteLineSep + "The file can be recovered by reverting it to it's last revisioned state." return note + os.linesep + "= Removed File Diffs" + os.linesep + text elif type == '!': note = bFileName + " was tracked but is now missing. If commited, it will be marked as dropped." note += noteLineSep + "The file can be recovered by reverting it to it's last revisioned state." return note + os.linesep + "= Contents of Missing File" + os.linesep + text elif type == '>': return text else: return "Unknown file type " + type def commitFiles(self, selectedFileList, logMsgText): '''Commit selected files''' # Files in list are annotated (A, M, etc) so this function can # mark files for add or delete as necessary before instigating the commit. commitFileNames = [] addFileList = [] removeFileList = [] for f in selectedFileList: type = f[0] fileName = f[2:] commitFileNames.append(fileName) if type in ['?', 'I']: addFileList.append(fileName) elif type == '!': removeFileList.append(fileName) if addFileList: runProgram([self.mtn_exe, 'add'] + addFileList) print "Added %d file(s) to revision control: %s" % (len(addFileList), ', '.join(addFileList)) if removeFileList: runProgram([self.mtn_exe, 'drop'] + removeFileList) print "Removed %d file(s) from revision control: %s" % (len(removeFileList), ', '.join(removeFileList)) (fd, filename) = mkstemp() file = os.fdopen(fd, "w+b") file.write(logMsgText) file.close() runProgram([self.mtn_exe, 'commit', '--message-file', filename] + commitFileNames) print "%d file(s) commited: %s" % (len(commitFileNames), ', '.join(commitFileNames)) def addFiles(self, selectedFileList): '''Add selected files to version control''' runProgram([self.mtn_exe, 'add'] + selectedFileList) def revertFiles(self, selectedFileList): '''Revert selected files to last revisioned state''' revertFileNames = [] for f in selectedFileList: type = f[0] fileName = f[2:] if type in ['R', '!', 'M']: prevState = self.stateNames[type] print "%s recovered to last revisioned state (was %s)" % (fileName, prevState) revertFileNames.append(fileName) elif type == 'A': print "%s removed from revision control (was added)" % fileName revertFileNames.append(fileName) elif type == '>': runProgram([self.mtn_exe, 'revert', fileName]) targetName = self.renameTarget[ fileName ] os.unlink(targetName) print "Rename of %s reverted, %s removed" % (fileName, targetName) else: print "File %s not reverted" % fileName if revertFileNames: runProgram([self.mtn_exe, 'revert'] + revertFileNames) # vim: tw=120 qct/qctlib/vcs/p4.py0000644000000000000000000002134611146115774014772 0ustar00usergroup00000000000000# Perfoce VCS back-end code for qct # # Copyright 2006 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. # Usage Notes: # o Assumes P4CLIENT, P4PORT, P4USER are properly set # o Will execute out of current directory, but all paths will be listed globally # o Will always use the default changelist from qctlib.utils import runProgram import os class qctVcsP4: def initRepo(self, argv): '''Initialize your revision control system, open repository''' # Verify we have a valid repository output = runProgram(['p4', 'fstat', '.'], expectedexits=[0,1]) if output.startswith("Path "): print output return -1 else: return 0 def capabilities(self): '''Return a list of optional capabilities supported by this VCS''' # Perforce support is pretty sparse return ( ) def getLogTemplate(self): '''Request default log message template from VCS''' if not os.environ.has_key('P4_LOG_TEMPLATE'): return '' logFileName = os.environ['P4_LOG_TEMPLATE'] try: f = open(logFileName) text = f.read() f.close() return text except IOError: return '' def getAutoSelectTypes(self): '''Return annotations of file types which should be automatically selected when a new commit is started''' return ['A', 'M', 'R'] def generateParentFile(self, workingFile): '''The GUI needs this file's parent revision in place so the user can select individual changes for commit (basically a revert) ''' runProgram(['p4', 'revert', self.clientToDepotMap[workingFile] ]) runProgram(['p4', 'edit', self.clientToDepotMap[workingFile] ]) def dirtyCache(self, fileName): '''The GUI would like us to forget cached data for this file''' if self.wdCache.has_key(fileName): del self.wdCache[fileName] def scanFiles(self, showIgnored): '''Request scan for all commitable files from VCS''' # Called at startup, when 'Refresh' button is pressed, or when # showIgnored toggled. fileList = [] self.wdCache = { } self.clientToDepotMap = { } # Find open files, this command returns depot paths statusOutput = runProgram(['p4', 'opened', '...']) recs = statusOutput.split(os.linesep) recs.pop() # remove last entry (which is '') for line in recs: if line.startswith('... '): break fileName = line.split('#')[0] # remove rev number and status results = getP4FileStatus(fileName) fileList.append(results[2] + ' ' + results[1]) self.clientToDepotMap[results[1]] = results[0] # Find missing files, this command returns client paths statusOutput = runProgram(['p4', 'diff', '-sd', '...']) recs = statusOutput.split(os.linesep) recs.pop() # remove last entry (which is '') for fileName in recs: # current directory may not be revisioned yet if fileName.startswith('... '): break results = getP4FileStatus(fileName) fileList.append('!' + ' ' + results[1]) self.clientToDepotMap[results[1]] = results[0] return fileList def getFileStatus(self, itemName): '''Request file deltas from VCS''' status = itemName[0] fileName = itemName[2:] bFileName = "%" + fileName + " " if status == 'M': if self.wdCache.has_key(fileName): return self.wdCache[fileName] else: text = runProgram(['p4', 'diff', '-du', self.clientToDepotMap[fileName] ]) self.wdCache[fileName] = text[:-len(os.linesep)] return text elif status == 'A': return bFileName + "has been added to perforce, but not yet commited" elif status == 'R': return bFileName + "has been opened for delete, but has not yet been commited" elif status == '!': return bFileName + "was tracked but is now missing, will be removed from perforce if commited" elif status == '*': return bFileName + "has no recorded open reason with perforce (what's going on?)" else: return "Unknown file type " + status def commitFiles(self, selectedFileList, logMsgText): '''Commit selected files''' # Files in list are annotated (A, M, etc) so this function can # mark files for add or delete as necessary before instigating the commit. depotFileNames = [] for f in selectedFileList: status = f[0] fileName = f[2:] if status == '?' or status == 'I': print "Adding %s to revision control" % fileName runProgram(['p4', 'add', fileName]) elif status == '!': print "Removing %s from revision control" % fileName runProgram(['p4', 'delete', self.clientToDepotMap[fileName] ]) if self.clientToDepotMap.has_key(fileName): depotFileNames.append(self.clientToDepotMap[fileName]) else: print "Unable to find depot name of " + fileName # Perforce doesn't allow you to specify a message on the command line. # What you have to do is pretend to do the commit and ask for the form it would # create. You then insert the log message and actual file list into that file and # then submit that with -i iform = runProgram(['p4', 'change', '-o']) recs = iform.split(os.linesep) oform = '' for line in recs: if line == '\t': logMsgLines = logMsgText.split(os.linesep) for l in logMsgLines: oform += '\t' + l + os.linesep elif line == 'Files:': oform += line + os.linesep for i in depotFileNames: oform += '\t' + i + os.linesep break else: oform += line + os.linesep # print "Output form that would be submitted to perforce: " + oform runProgram(['p4', 'submit', '-i'], oform) print "%d file(s) commited: %s" % (len(selectedFileList), ', '.join(depotFileNames)) def addFiles(self, selectedFileList): '''Add selected files to version control''' runProgram(['p4', 'add'] + selectedFileList) def revertFiles(self, selectedFileList): '''Revert selected files to last revisioned state''' revertFileNames = [] for f in selectedFileList: status = f[0] fileName = f[2:] if status == 'R': print "deleted %s recovered from revision control" % fileName revertFileNames.append(self.clientToDepotMap[fileName]) elif status == '!': print "missing %s recovered from revision control" % fileName runProgram(['p4', 'sync', '-f', self.clientToDepotMap[fileName] ]) elif status == 'A': print "added %s forgot from revision control" % fileName revertFileNames.append(self.clientToDepotMap[fileName]) elif status == 'M': print "modifications to %s reverted" % fileName revertFileNames.append(self.clientToDepotMap[fileName]) else: print "File %s not reverted" % fileName if len(revertFileNames): runProgram(['p4', 'revert'] + revertFileNames) else: print "No revertable files" def getP4FileStatus(fileName): '''Helper function which determines a file's open reason''' fstatOut = runProgram(['p4', 'fstat', fileName]) status = '*' # No open action # normalize case of pwd and client path, to have a good chance of this actually # working on windows cwd = os.path.normcase(os.getcwd()) afterCwd = len(cwd) + 1 recs = fstatOut.split(os.linesep) for line in recs: words = line.split(' ') if len(words) < 2: continue if words[1] == 'depotFile': depotName = words[2] elif words[1] == 'clientFile': clientName = os.path.normcase(words[2]) # Prune current working directory from client name (make relative) if clientName.startswith(cwd): clientName = clientName[afterCwd:] elif words[1] == 'action': if words[2] == 'add': status = 'A' elif words[2] == 'edit': status = 'M' elif words[2] == 'delete': status = 'R' return (depotName, clientName, status) # vim: tw=120 qct/qctlib/vcs/svn.py0000644000000000000000000002400011146115774015243 0ustar00usergroup00000000000000# Subversion back-end code for qct # # Copyright 2007 Steve Borho # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. # Usage Notes: # # * Assumes you have a working svn command line tool # * Assumes there is no passphrase required for rsh/ssh access from qctlib.utils import runProgram, isBinary from tempfile import mkstemp import os class qctVcsSvn: def initRepo(self, argv): '''Initialize your revision control system, open repository''' if not os.path.exists('.svn/'): print "No Subversion repository found" return -1 self.svn_exe = 'svn' self.stateNames = { 'M' : 'modified', 'R' : 'removed', '!' : 'missing', '?' : 'unknown' } return 0 def capabilities(self): '''Return a list of optional capabilities supported by this VCS''' return ( 'ignore', 'progressbar' ) def addIgnoreMask(self, newIgnoreMask): '''The user has provided a new ignoreMask to be added to revision control''' existingIgnores = runProgram([self.svn_exe, 'propget', 'svn:ignore', '.']).split(os.linesep) existingIgnores.pop() existingIgnores.append(newIgnoreMask) (fd, filename) = mkstemp() file = os.fdopen(fd, "w+b") file.write(os.linesep.join(existingIgnores)) file.close() runProgram([self.svn_exe, 'propset', '--file', filename, 'svn:ignore', '.']) def generateParentFile(self, workingFile): '''The GUI needs this file's parent revision in place so the user can select individual changes for commit (basically a revert) ''' runProgram([self.svn_exe, 'revert', workingFile]) def getLogTemplate(self): '''Request default log message template from VCS''' logFileName = os.path.expanduser('~/.commit.template') try: f = open(logFileName) text = f.read() f.close() return text except IOError: return '' def getAutoSelectTypes(self): '''Return annotations of file types which should be automatically selected when a new commit is started''' return ['A', 'M', 'R'] def dirtyCache(self, fileName): '''The GUI would like us to forget cached data for this file''' if self.wdDiffCache.has_key(fileName): del self.wdDiffCache[fileName] def scanFiles(self, showIgnored, pb = None): '''Request scan for all commitable files from VCS, with optional progress bar ''' # Called at startup and when 'Refresh' button is pressed self.wdDiffCache = {} self.fileStatus = {} itemList = [] if pb: pb.setValue(1) if showIgnored: extra = ['--no-ignore'] else: extra = [] statusOutput = runProgram([self.svn_exe, '--ignore-externals'] + extra + ['status']) recs = statusOutput.split(os.linesep) recs.pop() # remove last entry (which is '') if pb: pb.setValue(2) for line in recs: if len(line) < 7: continue status = line[0] fname = line[7:] self.fileStatus[ fname ] = line[0:6] if status == 'M': # modified itemList.append('M ' + fname) elif status == 'A': # added itemList.append('A ' + fname) elif status == '?': # unknown itemList.append('? ' + fname) elif status == '!': # missing itemList.append('! ' + fname) elif status == 'D': # deleted itemList.append('R ' + fname) elif status == 'C': # conflict (allow edit) itemList.append('C ' + fname) elif status in ('~', 'X', 'R'): # Skip these files, they are not commitable pass if pb: pb.setValue(3) return itemList def __getWorkingDirChanges(self, fileName, type): if self.wdDiffCache.has_key(fileName): return self.wdDiffCache[fileName] # For revisioned files, we use cvs diff if type in ['A', 'M', 'R']: text = runProgram([self.svn_exe, 'diff', fileName], expectedexits=[0,1]) self.wdDiffCache[fileName] = text return text elif type in ['!']: # Missing files can be retrieved with cat text = runProgram([self.svn_exe, 'cat', fileName], expectedexits=[0,1]) self.wdDiffCache[fileName] = text return text elif type in ['?', 'I']: # For unrevisioned files, we return file contents if os.path.isdir(fileName): text = " " fnames = os.listdir(fileName) text += os.linesep + ' ' + '\n '.join(fnames) elif isBinary(fileName): text = " " else: f = open(fileName) text = f.read() f.close() self.wdDiffCache[fileName] = text return text else: return "Unknown file type " + type def getFileStatus(self, itemName): '''Request file deltas from VCS''' type = itemName[0] fileName = itemName[2:] text = self.__getWorkingDirChanges(fileName, type) # Useful shorthand vars. Leading lines beginning with % are treated as RTF bFileName = "%" + fileName + "" noteLineSep = os.linesep + '%' if type == 'A': note = bFileName + " has been added to cvs, but has never been commited." return note + os.linesep + text elif type == 'M': note = bFileName + " has been modified in your working directory." return note + os.linesep + text elif type == '?': note = bFileName + " is not currently tracked. If commited, it will be added to revision control." return note + os.linesep + "= Unrevisioned File Contents" + os.linesep + text elif type == 'I': note = bFileName + " is usually ignored, but will be added to revision control if commited" return note + os.linesep + text elif type == 'R': note = bFileName + " has been marked for deletion, or renamed, but has not yet been commited" note += noteLineSep + "The file can be recovered by reverting it to it's last revisioned state." return note + os.linesep + "= Removed File Diffs" + os.linesep + text elif type == '!': note = bFileName + " was tracked but is now missing. If commited, it will be marked as removed in cvs." note += noteLineSep + "The file can be recovered by reverting it to it's last revisioned state." return note + os.linesep + "= Contents of Missing File" + os.linesep + text else: return "Unknown file type " + type def commitFiles(self, selectedFileList, logMsgText): '''Commit selected files''' # Files in list are annotated (A, M, etc) so this function can # mark files for add or delete as necessary before instigating the commit. commitFileNames = [] dirList = [] addFileList = [] binaryAddFileList = [] removeFileList = [] for f in selectedFileList: type = f[0] fileName = f[2:] commitFileNames.append(fileName) if type in ['?', 'I']: if os.path.isdir(fileName): dirList.append(fileName) elif isBinary(fileName): binaryAddFileList.append(fileName) else: addFileList.append(fileName) elif type == '!': removeFileList.append(fileName) if dirList: # Sort added directories by name length, to avoid recursion problems dirList.sort(lambda x,y: cmp(len(x), len(y))) runProgram([self.svn_exe, 'add', '--non-recursive'] + dirList) print "Added %d directory(s) to revision control: %s" % (len(dirList), ', '.join(dirList)) if binaryAddFileList: runProgram([self.svn_exe, 'add'] + addFileList) print "Added %d binary file(s) to revision control: %s" % (len(addFileList), ', '.join(addFileList)) if addFileList: runProgram([self.svn_exe, 'add'] + addFileList) print "Added %d file(s) to revision control: %s" % (len(addFileList), ', '.join(addFileList)) if removeFileList: runProgram([self.svn_exe, 'delete'] + removeFileList) print "Removed %d file(s) from revision control: %s" % (len(removeFileList), ', '.join(removeFileList)) (fd, filename) = mkstemp() file = os.fdopen(fd, "w+b") file.write(logMsgText) file.close() runProgram([self.svn_exe, 'commit', '-F', filename] + commitFileNames) print "%d file(s) commited: %s" % (len(commitFileNames), ', '.join(commitFileNames)) def addFiles(self, selectedFileList): '''Add selected files to version control''' runProgram([self.svn_exe, 'add'] + selectedFileList) def revertFiles(self, selectedFileList): '''Revert selected files to last revisioned state''' revertFileNames = [] for f in selectedFileList: type = f[0] fileName = f[2:] if type in ['R', '!', 'M']: prevState = self.stateNames[type] print "%s recovered to last revisioned state (was %s)" % (fileName, prevState) revertFileNames.append(fileName) elif type == 'A': print "%s removed from revision control (was added)" % fileName revertFileNames.append(fileName) else: print "File %s not reverted" % fileName if revertFileNames: runProgram([self.svn_exe, 'revert'] + revertFileNames) else: print "No revertable files" # vim: tw=120 qct/qctlib/version.py0000644000000000000000000000002411146115774015327 0ustar00usergroup00000000000000qct_version = '1.7' qct/setup.py0000755000000000000000000000763011146115774013541 0ustar00usergroup00000000000000#!/usr/bin/env python # # This is the qct setup script. # # './setup.py install', or # './setup.py --help' for more options import sys, os if not hasattr(sys, 'version_info') or sys.version_info < (2, 4, 0, 'final'): raise SystemExit, "Qct requires python 2.4 or later." extra = {} try: # to generate qct MSI installer, you run python setup.py bdist_msi from setuptools import setup if os.name in ['nt']: # the msi will automatically install the qct.py plugin into hgext extra['data_files'] = [('lib/site-packages/hgext', ['hgext/qct.py']), ('mercurial/hgrc.d', ['qct.rc']), ('share/qct', ['doc/qct.1.html', 'README', 'README.mercurial'])] extra['scripts'] = ['win32/qct_postinstall.py'] else: extra['scripts'] = ['qct'] except ImportError: from distutils.core import setup extra['scripts'] = ['qct'] from distutils.command.build import build from distutils.spawn import find_executable, spawn from qctlib.version import qct_version try: import py2app extra['app'] = ['qct'] except ImportError: pass try: # to generate qct.exe, you need python 2.5, Qt 4.2, PyQt4, and py2exe # then execute: python setup.py py2exe import py2exe extra['console'] = ['qct'] except ImportError: pass opts = { "py2exe" : { "excludes" : "pywin,pywin.dialogs,pywin.dialogs.list", "includes" : "sip" # ",PyQt4._qt" } } class QctBuild(build): def compile_ui(self, ui_file, py_file): # Search for pyuic4 in python bin dir, then in the $Path. try: from PyQt4 import uic fp = open(py_file, 'w') uic.compileUi(ui_file, fp) fp.close() except Exception, e: print 'Unable to compile user interface', e return def run(self): if not os.path.exists('qctlib/ui_dialog.py'): self.compile_ui('qctlib/dialog.ui', 'qctlib/ui_dialog.py') if not os.path.exists('qctlib/ui_preferences.py'): self.compile_ui('qctlib/preferences.ui', 'qctlib/ui_preferences.py') if not os.path.exists('qctlib/ui_select.py'): self.compile_ui('qctlib/select.ui', 'qctlib/ui_select.py') build.run(self) setup(name='qct', version=qct_version, download_url='http://qct.sourceforce.net/qct-' + qct_version + '.tar.gz', author='Steve Borho', author_email='steve@borho.org', url='http://qct.sourceforge.net', description='Commit Tool', long_description=''' Qct Qt4/PyQt based commit tool Primary goals: 1) Cross-Platform (Linux, Windows-Native, MacOS) 2) Be VCS agnostic 3) Good keyboard navigation, keep the typical work-flow simple 4) Universal change selection Currently supports Mercurial, Bazaar, CVS, and Monotone repositories.''', license='GNU GPL2', packages=['qctlib', 'qctlib/vcs'], cmdclass = { 'build' : QctBuild }, # define custom build class classifiers=['Development Status :: 4 - Beta', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'Intended Audience :: System Administrators', 'Natural Language :: English', 'Environment :: Console', 'Environment :: MacOS X', 'Operating System :: OS Independent', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Operating System :: Unix', 'Programming Language :: Python', 'Topic :: Software Development', 'Topic :: System :: Systems Administration', 'Topic :: Software Development :: Version Control'], platforms='All', options=opts, **extra) qct/win32/qct.bat0000644000000000000000000000034411146115774014240 0ustar00usergroup00000000000000@echo off rem You should change the VCS to the one you use: rem --hg - Mercurial rem --git - Git rem --bzr - Bazaar rem --p4 - Perforce rem --svn - Subversion python C:\Python25\Scripts\qct --p4 %* qct/win32/qct.iss0000644000000000000000000000313211146115774014266 0ustar00usergroup00000000000000; Script generated by the Inno Setup Script Wizard. ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "qct" #define MyAppVerName "Qct-1.7" #define MyAppPublisher "Steve Borho" #define MyAppURL "http://qct.sourceforge.net/" #define MyAppExeName "qct.exe" [Setup] AppName={#MyAppName} AppVerName={#MyAppVerName} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={pf}\{#MyAppName} DefaultGroupName=Qct AllowNoIcons=yes LicenseFile=..\COPYING OutputDir=release OutputBaseFilename={#MyAppVerName}-standalone-win32 Compression=lzma SolidCompression=yes [Languages] Name: english; MessagesFile: compiler:Default.isl [Files] Source: ..\dist\qct.exe; DestDir: {app}; Flags: ignoreversion Source: ..\dist\library.zip; DestDir: {app}; Flags: ignoreversion Source: ..\dist\*.dll; DestDir: {app}; Flags: ignoreversion Source: ..\dist\*.pyd; DestDir: {app}; Flags: ignoreversion Source: ..\dist\w9xpopen.exe; DestDir: {app}; Flags: ignoreversion ;Source: ..\dist\mercurial\*; DestDir: {app}\mercurial; Flags: ignoreversion recursesubdirs createallsubdirs Source: ..\hgext\qct.py; DestDir: {app}\mercurial; Flags: ignoreversion ;Source: ..\dist\qct\*; DestDir: {app}\docs; Flags: ignoreversion ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] Name: {group}\{#MyAppName}; Filename: {app}\{#MyAppExeName} Name: {group}\{cm:ProgramOnTheWeb,{#MyAppName}}; Filename: {#MyAppURL} Name: {group}\{cm:UninstallProgram,{#MyAppName}}; Filename: {uninstallexe} qct/win32/qct_postinstall.py0000644000000000000000000000121111146115774016550 0ustar00usergroup00000000000000# Post-install script for Qct Windows MSI # Configure default values for Qct preferences from _winreg import HKEY_LOCAL_MACHINE, REG_SZ, CreateKey, SetValueEx, QueryValueEx key = CreateKey(HKEY_LOCAL_MACHINE, 'Software\\vcs\\Qct\\tools') try: value, type = QueryValueEx(key, 'diffTool') except WindowsError: SetValueEx(key, 'diffTool', 0, REG_SZ, 'hg vdiff') try: value, type = QueryValueEx(key, 'histTool') except WindowsError: SetValueEx(key, 'histTool', 0, REG_SZ, 'hg view') try: value, type = QueryValueEx(key, 'editTool') except WindowsError: SetValueEx(key, 'editTool', 0, REG_SZ, 'notepad')